diff --git a/Makefile b/Makefile index cb52685..aa3d9f9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ +.PHONY: run test version update-mongo + run: echo "This is a library - can't be run" && false @@ -8,4 +10,7 @@ test: gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test" version: - _data/version.sh \ No newline at end of file + _data/version.sh + +update-mongo: + _data/update-mongo.sh \ No newline at end of file diff --git a/_data/update-mongo.sh b/_data/update-mongo.sh new file mode 100755 index 0000000..b3f45f0 --- /dev/null +++ b/_data/update-mongo.sh @@ -0,0 +1,72 @@ +#!/bin/bash + + + set -o nounset # disallow usage of unset vars ( set -u ) + set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) + set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) + set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status + IFS=$'\n\t' # Set $IFS to only newline and tab. + + + +dir="/tmp/mongo_repo_$( uuidgen )" + +git clone "https://github.com/mongodb/mongo-go-driver" "$dir" + +pushd "$dir" + +git fetch --tags + +latestTag="$( git describe --tags `git rev-list --tags --max-count=1` )" + +git -c "advice.detachedHead=false" checkout $latestTag + +latestSHA="$( git rev-parse HEAD )" + +popd + +existingTag=$( cat mongoPatchVersion.go | grep -oP "(?<=const MongoCloneTag = \")([A-Za-z0-9.]+)(?=\")" ) +existingSHA=$( cat mongoPatchVersion.go | grep -oP "(?<=const MongoCloneCommit = \")([A-Za-z0-9.]+)(?=\")" ) + +echo "====================================" +echo "ID (online) $latestSHA" +echo "ID (local) $existingSHA" +echo "Tag (online) $latestTag" +echo "Tag (local) $existingTag" +echo "====================================" + +if [[ "$latestTag" == "$existingTag" ]]; then + echo "Nothing to do" + rm -rf "$dir" + exit 0 +fi + +rm -rf mongo +cp -r "$dir" "mongo" +rm -rf "mongo/.git" +rm -rf "mongo/.evergreen" +rm -rf "mongo/cmd" +rm -rf "mongo/docs" +rm -rf "mongo/etc" +rm -rf "mongo/examples" +rm -rf "mongo/testdata" +rm -rf "mongo/benchmark" +rm -rf "mongo/vendor" +rm -rf "mongo/go.mod" +rm -rf "mongo/go.sum" + +{ + + printf "package goext\n" + printf "\n" + printf "// %s\n" "$( date +"%Y-%m-%d %H:%M:%S%z" )" + printf "\n" + printf "const MongoCloneTag = \"%s\"\n" "$latestTag" + printf "const MongoCloneCommit = \"%s\"\n" "$latestSHA" + +} > mongoPatchVersion.go + +rm -rf "$dir" + +echo "" +echo "Done." \ No newline at end of file diff --git a/mongo/.errcheck-excludes b/mongo/.errcheck-excludes new file mode 100644 index 0000000..bd8b124 --- /dev/null +++ b/mongo/.errcheck-excludes @@ -0,0 +1,16 @@ +(go.mongodb.org/mongo-driver/x/mongo/driver.Connection).Close +(*go.mongodb.org/mongo-driver/x/network/connection.connection).Close +(go.mongodb.org/mongo-driver/x/network/connection.Connection).Close +(*go.mongodb.org/mongo-driver/x/mongo/driver/topology.connection).close +(*go.mongodb.org/mongo-driver/x/mongo/driver/topology.Topology).Unsubscribe +(*go.mongodb.org/mongo-driver/x/mongo/driver/topology.Server).Close +(*go.mongodb.org/mongo-driver/x/network/connection.pool).closeConnection +(*go.mongodb.org/mongo-driver/x/mongo/driver/topology.pool).close +(go.mongodb.org/mongo-driver/x/network/wiremessage.ReadWriteCloser).Close +(*go.mongodb.org/mongo-driver/mongo.Cursor).Close +(*go.mongodb.org/mongo-driver/mongo.ChangeStream).Close +(*go.mongodb.org/mongo-driver/mongo.Client).Disconnect +(net.Conn).Close +encoding/pem.Encode +fmt.Fprintf +fmt.Fprint diff --git a/mongo/.gitignore b/mongo/.gitignore new file mode 100644 index 0000000..ba9d126 --- /dev/null +++ b/mongo/.gitignore @@ -0,0 +1,13 @@ +.vscode +debug +.idea +*.iml +*.ipr +*.iws +.idea +*.sublime-project +*.sublime-workspace +driver-test-data.tar.gz +perf +**mongocryptd.pid +*.test diff --git a/mongo/.gitmodules b/mongo/.gitmodules new file mode 100644 index 0000000..e5d2a68 --- /dev/null +++ b/mongo/.gitmodules @@ -0,0 +1,3 @@ +[submodule "specifications"] + path = specifications + url = git@github.com:mongodb/specifications.git diff --git a/mongo/.golangci.yml b/mongo/.golangci.yml new file mode 100644 index 0000000..061d2ff --- /dev/null +++ b/mongo/.golangci.yml @@ -0,0 +1,123 @@ +run: + timeout: 5m + +linters: + disable-all: true + # TODO(GODRIVER-2156): Enable all commented-out linters. + enable: + - errcheck + # - errorlint + - gocritic + - goimports + - gosimple + - gosec + - govet + - ineffassign + - makezero + - misspell + - nakedret + - paralleltest + - prealloc + - revive + - staticcheck + - typecheck + - unused + - unconvert + - unparam + +linters-settings: + errcheck: + exclude: .errcheck-excludes + gocritic: + enabled-checks: + # Detects suspicious append result assignments. E.g. "b := append(a, 1, 2, 3)" + - appendAssign + govet: + disable: + - cgocall + - composites + paralleltest: + # Ignore missing calls to `t.Parallel()` and only report incorrect uses of `t.Parallel()`. + ignore-missing: true + staticcheck: + checks: [ + "all", + "-SA1019", # Disable deprecation warnings for now. + "-SA1012", # Disable "do not pass a nil Context" to allow testing nil contexts in tests. + ] + +issues: + exclude-use-default: false + exclude: + # Add all default excluded issues except issues related to exported types/functions not having + # comments; we want those warnings. The defaults are copied from the "--exclude-use-default" + # documentation on https://golangci-lint.run/usage/configuration/#command-line-options + ## Defaults ## + # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + # EXC0003 golint: False positive when tests are defined in package 'test' + - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this + # EXC0004 govet: Common false positives + - (possible misuse of unsafe.Pointer|should have signature) + # EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore + - ineffective break statement. Did you mean to break out of the outer loop + # EXC0006 gosec: Too many false-positives on 'unsafe' usage + - Use of unsafe calls should be audited + # EXC0007 gosec: Too many false-positives for parametrized shell calls + - Subprocess launch(ed with variable|ing should be audited) + # EXC0008 gosec: Duplicated errcheck checks + - (G104|G307) + # EXC0009 gosec: Too many issues in popular repos + - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) + # EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' + - Potential file inclusion via variable + ## End Defaults ## + + # Ignore capitalization warning for this weird field name. + - "var-naming: struct field CqCssWxW should be CqCSSWxW" + # Ignore warnings for common "wiremessage.Read..." usage because the safest way to use that API + # is by assigning possibly unused returned byte buffers. + - "SA4006: this value of `wm` is never used" + - "SA4006: this value of `rem` is never used" + - "ineffectual assignment to wm" + - "ineffectual assignment to rem" + + skip-dirs-use-default: false + skip-dirs: + - (^|/)vendor($|/) + - (^|/)testdata($|/) + - (^|/)etc($|/) + exclude-rules: + # Ignore some linters for example code that is intentionally simplified. + - path: examples/ + linters: + - revive + - errcheck + # Disable unused code linters for the copy/pasted "awsv4" package. + - path: x/mongo/driver/auth/internal/awsv4 + linters: + - unused + # Disable "unused" linter for code files that depend on the "mongocrypt.MongoCrypt" type because + # the linter build doesn't work correctly with CGO enabled. As a result, all calls to a + # "mongocrypt.MongoCrypt" API appear to always panic (see mongocrypt_not_enabled.go), leading + # to confusing messages about unused code. + - path: x/mongo/driver/crypt.go|mongo/(crypt_retrievers|mongocryptd).go + linters: + - unused + # Ignore "TLS MinVersion too low", "TLS InsecureSkipVerify set true", and "Use of weak random + # number generator (math/rand instead of crypto/rand)" in tests. + - path: _test\.go + text: G401|G402|G404 + linters: + - gosec + # Ignore missing comments for exported variable/function/type for code in the "internal" and + # "benchmark" directories. + - path: (internal\/|benchmark\/) + text: exported (.+) should have comment( \(or a comment on this block\))? or be unexported + # Ignore missing package comments for directories that aren't frequently used by external users. + - path: (internal\/|benchmark\/|x\/|cmd\/|mongo\/integration\/) + text: should have a package comment + # Disable unused linter for "golang.org/x/exp/rand" package in internal/randutil/rand. + - path: internal/randutil/rand + linters: + - unused diff --git a/mongo/LICENSE b/mongo/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/mongo/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/mongo/Makefile b/mongo/Makefile new file mode 100644 index 0000000..a5796c9 --- /dev/null +++ b/mongo/Makefile @@ -0,0 +1,210 @@ +ATLAS_URIS = "$(ATLAS_FREE)" "$(ATLAS_REPLSET)" "$(ATLAS_SHARD)" "$(ATLAS_TLS11)" "$(ATLAS_TLS12)" "$(ATLAS_FREE_SRV)" "$(ATLAS_REPLSET_SRV)" "$(ATLAS_SHARD_SRV)" "$(ATLAS_TLS11_SRV)" "$(ATLAS_TLS12_SRV)" "$(ATLAS_SERVERLESS)" "$(ATLAS_SERVERLESS_SRV)" +TEST_TIMEOUT = 1800 + +### Utility targets. ### +.PHONY: default +default: build check-license check-fmt check-modules lint test-short + +.PHONY: add-license +add-license: + etc/check_license.sh -a + +.PHONY: check-license +check-license: + etc/check_license.sh + +.PHONY: build +build: cross-compile build-tests build-compile-check + go build ./... + go build $(BUILD_TAGS) ./... + +# Use ^$ to match no tests so that no tests are actually run but all tests are +# compiled. Run with -short to ensure none of the TestMain functions try to +# connect to a server. +.PHONY: build-tests +build-tests: + go test -short $(BUILD_TAGS) -run ^$$ ./... + +.PHONY: build-compile-check +build-compile-check: + etc/compile_check.sh + +# Cross-compiling on Linux for architectures 386, arm, arm64, amd64, ppc64le, and s390x. +# Omit any build tags because we don't expect our build environment to support compiling the C +# libraries for other architectures. +.PHONY: cross-compile +cross-compile: + GOOS=linux GOARCH=386 go build ./... + GOOS=linux GOARCH=arm go build ./... + GOOS=linux GOARCH=arm64 go build ./... + GOOS=linux GOARCH=amd64 go build ./... + GOOS=linux GOARCH=ppc64le go build ./... + GOOS=linux GOARCH=s390x go build ./... + +.PHONY: install-lll +install-lll: + go install github.com/walle/lll/...@latest + +.PHONY: check-fmt +check-fmt: install-lll + etc/check_fmt.sh + +# check-modules runs "go mod tidy" then "go mod vendor" and exits with a non-zero exit code if there +# are any module or vendored modules changes. The intent is to confirm two properties: +# +# 1. Exactly the required modules are declared as dependencies. We should always be able to run +# "go mod tidy" and expect that no unrelated changes are made to the "go.mod" file. +# +# 2. All required modules are copied into the vendor/ directory and are an exact copy of the +# original module source code (i.e. the vendored modules are not modified from their original code). +.PHONY: check-modules +check-modules: + go mod tidy -v + go mod vendor + git diff --exit-code go.mod go.sum ./vendor + +.PHONY: doc +doc: + godoc -http=:6060 -index + +.PHONY: fmt +fmt: + go fmt ./... + +.PHONY: install-golangci-lint +install-golangci-lint: + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0 + +# Lint with various GOOS and GOARCH targets to catch static analysis failures that may only affect +# specific operating systems or architectures. For example, staticcheck will only check for 64-bit +# alignment of atomically accessed variables on 32-bit architectures (see +# https://staticcheck.io/docs/checks#SA1027) +.PHONY: lint +lint: install-golangci-lint + GOOS=linux GOARCH=386 golangci-lint run --config .golangci.yml ./... + GOOS=linux GOARCH=arm golangci-lint run --config .golangci.yml ./... + GOOS=linux GOARCH=arm64 golangci-lint run --config .golangci.yml ./... + GOOS=linux GOARCH=amd64 golangci-lint run --config .golangci.yml ./... + GOOS=linux GOARCH=ppc64le golangci-lint run --config .golangci.yml ./... + GOOS=linux GOARCH=s390x golangci-lint run --config .golangci.yml ./... + +.PHONY: update-notices +update-notices: + etc/generate_notices.pl > THIRD-PARTY-NOTICES + +### Local testing targets. ### +.PHONY: test +test: + go test $(BUILD_TAGS) -timeout $(TEST_TIMEOUT)s -p 1 ./... + +.PHONY: test-cover +test-cover: + go test $(BUILD_TAGS) -timeout $(TEST_TIMEOUT)s -cover $(COVER_ARGS) -p 1 ./... + +.PHONY: test-race +test-race: + go test $(BUILD_TAGS) -timeout $(TEST_TIMEOUT)s -race -p 1 ./... + +.PHONY: test-short +test-short: + go test $(BUILD_TAGS) -timeout 60s -short ./... + +### Evergreen specific targets. ### +.PHONY: build-aws-ecs-test +build-aws-ecs-test: + go build $(BUILD_TAGS) ./cmd/testaws/main.go + +.PHONY: evg-test +evg-test: + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s -p 1 ./... >> test.suite + +.PHONY: evg-test-atlas +evg-test-atlas: + go run ./cmd/testatlas/main.go $(ATLAS_URIS) + +.PHONY: evg-test-atlas-data-lake +evg-test-atlas-data-lake: + ATLAS_DATA_LAKE_INTEGRATION_TEST=true go test -v ./mongo/integration -run TestUnifiedSpecs/atlas-data-lake-testing >> spec_test.suite + ATLAS_DATA_LAKE_INTEGRATION_TEST=true go test -v ./mongo/integration -run TestAtlasDataLake >> spec_test.suite + +.PHONY: evg-test-enterprise-auth +evg-test-enterprise-auth: + go run -tags gssapi ./cmd/testentauth/main.go + +.PHONY: evg-test-kmip +evg-test-kmip: + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionSpec/kmipKMS >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse/data_key_and_double_encryption >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse/corpus >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse/custom_endpoint >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse/kms_tls_options_test >> test.suite + +.PHONY: evg-test-kms +evg-test-kms: + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse/kms_tls_tests >> test.suite + +.PHONY: evg-test-load-balancers +evg-test-load-balancers: + # Load balancer should be tested with all unified tests as well as tests in the following + # components: retryable reads, retryable writes, change streams, initial DNS seedlist discovery. + go test $(BUILD_TAGS) ./mongo/integration -run TestUnifiedSpecs/retryable-reads -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestRetryableWritesSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestChangeStreamSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestInitialDNSSeedlistDiscoverySpec/load_balanced -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestLoadBalancerSupport -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration/unified -run TestUnifiedSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + +.PHONY: evg-test-ocsp +evg-test-ocsp: + go test -v ./mongo -run TestOCSP $(OCSP_TLS_SHOULD_SUCCEED) >> test.suite + +.PHONY: evg-test-serverless +evg-test-serverless: + # Serverless should be tested with all unified tests as well as tests in the following components: CRUD, load balancer, + # retryable reads, retryable writes, sessions, transactions and cursor behavior. + go test $(BUILD_TAGS) ./mongo/integration -run TestCrudSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestWriteErrorsWithLabels -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestWriteErrorsDetails -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestHintErrors -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestWriteConcernError -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestErrorsCodeNamePropagated -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestLoadBalancerSupport -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestUnifiedSpecs/retryable-reads -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestRetryableReadsProse -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestRetryableWritesSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestRetryableWritesProse -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestUnifiedSpecs/sessions -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestSessionsProse -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestUnifiedSpecs/transactions/legacy -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestConvenientTransactions -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration -run TestCursor -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test $(BUILD_TAGS) ./mongo/integration/unified -run TestUnifiedSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionSpec >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration -run TestClientSideEncryptionProse >> test.suite + +.PHONY: evg-test-versioned-api +evg-test-versioned-api: + # Versioned API related tests are in the mongo, integration and unified packages. + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration >> test.suite + go test -exec "env PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH)" $(BUILD_TAGS) -v -timeout $(TEST_TIMEOUT)s ./mongo/integration/unified >> test.suite + +.PHONY: build-gcpkms-test +build-gcpkms-test: + go build $(BUILD_TAGS) ./cmd/testgcpkms + +### Benchmark specific targets and support. ### +.PHONY: benchmark +benchmark:perf + go test $(BUILD_TAGS) -benchmem -bench=. ./benchmark + +.PHONY: driver-benchmark +driver-benchmark:perf + @go run cmd/godriver-benchmark/main.go | tee perf.suite + +perf:driver-test-data.tar.gz + tar -zxf $< $(if $(eq $(UNAME_S),Darwin),-s , --transform=s)/testdata/perf/ + @touch $@ + +driver-test-data.tar.gz: + curl --retry 5 "https://s3.amazonaws.com/boxes.10gen.com/build/driver-test-data.tar.gz" -o driver-test-data.tar.gz --silent --max-time 120 diff --git a/mongo/README.md b/mongo/README.md new file mode 100644 index 0000000..682f720 --- /dev/null +++ b/mongo/README.md @@ -0,0 +1,251 @@ +

+

+ + docs + docs + +

+ +# MongoDB Go Driver + +The MongoDB supported driver for Go. + +------------------------- +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Feedback](#feedback) +- [Testing / Development](#testing--development) +- [Continuous Integration](#continuous-integration) +- [License](#license) + +------------------------- +## Requirements + +- Go 1.13 or higher. We aim to support the latest versions of Go. + - Go 1.20 or higher is required to run the driver test suite. +- MongoDB 3.6 and higher. + +------------------------- +## Installation + +The recommended way to get started using the MongoDB Go driver is by using Go modules to install the dependency in +your project. This can be done either by importing packages from `go.mongodb.org/mongo-driver` and having the build +step install the dependency or by explicitly running + +```bash +go get go.mongodb.org/mongo-driver/mongo +``` + +When using a version of Go that does not support modules, the driver can be installed using `dep` by running + +```bash +dep ensure -add "go.mongodb.org/mongo-driver/mongo" +``` + +------------------------- +## Usage + +To get started with the driver, import the `mongo` package and create a `mongo.Client` with the `Connect` function: + +```go +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) + +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() +client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017")) +``` + +Make sure to defer a call to `Disconnect` after instantiating your client: + +```go +defer func() { + if err = client.Disconnect(ctx); err != nil { + panic(err) + } +}() +``` + +For more advanced configuration and authentication, see the [documentation for mongo.Connect](https://pkg.go.dev/go.mongodb.org/mongo-driver/mongo#Connect). + +Calling `Connect` does not block for server discovery. If you wish to know if a MongoDB server has been found and connected to, +use the `Ping` method: + +```go +ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) +defer cancel() +err = client.Ping(ctx, readpref.Primary()) +``` + +To insert a document into a collection, first retrieve a `Database` and then `Collection` instance from the `Client`: + +```go +collection := client.Database("testing").Collection("numbers") +``` + +The `Collection` instance can then be used to insert documents: + +```go +ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() +res, err := collection.InsertOne(ctx, bson.D{{"name", "pi"}, {"value", 3.14159}}) +id := res.InsertedID +``` + +To use `bson.D`, you will need to add `"go.mongodb.org/mongo-driver/bson"` to your imports. + +Your import statement should now look like this: + +```go +import ( + "context" + "log" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) +``` + +Several query methods return a cursor, which can be used like this: + +```go +ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +cur, err := collection.Find(ctx, bson.D{}) +if err != nil { log.Fatal(err) } +defer cur.Close(ctx) +for cur.Next(ctx) { + var result bson.D + err := cur.Decode(&result) + if err != nil { log.Fatal(err) } + // do something with result.... +} +if err := cur.Err(); err != nil { + log.Fatal(err) +} +``` + +For methods that return a single item, a `SingleResult` instance is returned: + +```go +var result struct { + Value float64 +} +filter := bson.D{{"name", "pi"}} +ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() +err = collection.FindOne(ctx, filter).Decode(&result) +if err == mongo.ErrNoDocuments { + // Do something when no record was found + fmt.Println("record does not exist") +} else if err != nil { + log.Fatal(err) +} +// Do something with result... +``` + +Additional examples and documentation can be found under the examples directory and [on the MongoDB Documentation website](https://www.mongodb.com/docs/drivers/go/current/). + +------------------------- +## Feedback + +For help with the driver, please post in the [MongoDB Community Forums](https://developer.mongodb.com/community/forums/tag/golang/). + +New features and bugs can be reported on jira: https://jira.mongodb.org/browse/GODRIVER + +------------------------- +## Testing / Development + +The driver tests can be run against several database configurations. The most simple configuration is a standalone mongod with no auth, no ssl, and no compression. To run these basic driver tests, make sure a standalone MongoDB server instance is running at localhost:27017. To run the tests, you can run `make` (on Windows, run `nmake`). This will run coverage, run go-lint, run go-vet, and build the examples. + +### Testing Different Topologies + +To test a **replica set** or **sharded cluster**, set `MONGODB_URI=""` for the `make` command. +For example, for a local replica set named `rs1` comprised of three nodes on ports 27017, 27018, and 27019: + +``` +MONGODB_URI="mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs1" make +``` + +### Testing Auth and TLS + +To test authentication and TLS, first set up a MongoDB cluster with auth and TLS configured. Testing authentication requires a user with the `root` role on the `admin` database. Here is an example command that would run a mongod with TLS correctly configured for tests. Either set or replace PATH_TO_SERVER_KEY_FILE and PATH_TO_CA_FILE with paths to their respective files: + +``` +mongod \ +--auth \ +--tlsMode requireTLS \ +--tlsCertificateKeyFile $PATH_TO_SERVER_KEY_FILE \ +--tlsCAFile $PATH_TO_CA_FILE \ +--tlsAllowInvalidCertificates +``` + +To run the tests with `make`, set: +- `MONGO_GO_DRIVER_CA_FILE` to the location of the CA file used by the database +- `MONGO_GO_DRIVER_KEY_FILE` to the location of the client key file +- `MONGO_GO_DRIVER_PKCS8_ENCRYPTED_KEY_FILE` to the location of the pkcs8 client key file encrypted with the password string: `password` +- `MONGO_GO_DRIVER_PKCS8_UNENCRYPTED_KEY_FILE` to the location of the unencrypted pkcs8 key file +- `MONGODB_URI` to the connection string of the server +- `AUTH=auth` +- `SSL=ssl` + +For example: + +``` +AUTH=auth SSL=ssl \ +MONGO_GO_DRIVER_CA_FILE=$PATH_TO_CA_FILE \ +MONGO_GO_DRIVER_KEY_FILE=$PATH_TO_CLIENT_KEY_FILE \ +MONGO_GO_DRIVER_PKCS8_ENCRYPTED_KEY_FILE=$PATH_TO_ENCRYPTED_KEY_FILE \ +MONGO_GO_DRIVER_PKCS8_UNENCRYPTED_KEY_FILE=$PATH_TO_UNENCRYPTED_KEY_FILE \ +MONGODB_URI="mongodb://user:password@localhost:27017/?authSource=admin" \ +make +``` + +Notes: +- The `--tlsAllowInvalidCertificates` flag is required on the server for the test suite to work correctly. +- The test suite requires the auth database to be set with `?authSource=admin`, not `/admin`. + +### Testing Compression + +The MongoDB Go Driver supports wire protocol compression using Snappy, zLib, or zstd. To run tests with wire protocol compression, set `MONGO_GO_DRIVER_COMPRESSOR` to `snappy`, `zlib`, or `zstd`. For example: + +``` +MONGO_GO_DRIVER_COMPRESSOR=snappy make +``` + +Ensure the [`--networkMessageCompressors` flag](https://www.mongodb.com/docs/manual/reference/program/mongod/#cmdoption-mongod-networkmessagecompressors) on mongod or mongos includes `zlib` if testing zLib compression. + +------------------------- +## Contribution + +Check out the [project page](https://jira.mongodb.org/browse/GODRIVER) for tickets that need completing. See our [contribution guidelines](docs/CONTRIBUTING.md) for details. + +------------------------- +## Continuous Integration + +Commits to master are run automatically on [evergreen](https://evergreen.mongodb.com/waterfall/mongo-go-driver). + +------------------------- +## Frequently Encountered Issues + +See our [common issues](docs/common-issues.md) documentation for troubleshooting frequently encountered issues. + +------------------------- +## Thanks and Acknowledgement + +@ashleymcnamara - Mongo Gopher Artwork + +------------------------- +## License + +The MongoDB Go Driver is licensed under the [Apache License](LICENSE). diff --git a/mongo/THIRD-PARTY-NOTICES b/mongo/THIRD-PARTY-NOTICES new file mode 100644 index 0000000..7f0ded9 --- /dev/null +++ b/mongo/THIRD-PARTY-NOTICES @@ -0,0 +1,1554 @@ +---------------------------------------------------------------------- +License notice for AWS V4 signing code from github.com/aws/aws-sdk-go +AWS SDK for Go +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2014-2015 Stripe, Inc. +---------------------------------------------------------------------- + + + 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. + +--------------------------------------------------------------------- +License notice for gopkg.in/mgo.v2/bson +--------------------------------------------------------------------- + +BSON library for Go + +Copyright (c) 2010-2013 - Gustavo Niemeyer + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. 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 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. + +--------------------------------------------------------------------- +License notice for JSON and CSV code from github.com/golang/go +--------------------------------------------------------------------- + +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. + +--------------------------------------------------------------------- +License notice for rand code from golang.org/x/exp +--------------------------------------------------------------------- + +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. + +--------------------------------------------------------------------- +License notice for Add64 and Mul64 code from github.com/golang/go +--------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for github.com/davecgh/go-spew +---------------------------------------------------------------------- + +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------------------------------------------------------------- +License notice for github.com/golang/snappy +---------------------------------------------------------------------- + +Copyright (c) 2011 The Snappy-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. + +---------------------------------------------------------------------- +License notice for github.com/google/go-cmp +---------------------------------------------------------------------- + +Copyright (c) 2017 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. + +---------------------------------------------------------------------- +License notice for github.com/klauspost/compress +---------------------------------------------------------------------- + +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2019 Klaus Post. 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. + +---------------------------------------------------------------------- +License notice for github.com/klauspost/compress/snappy +---------------------------------------------------------------------- + +Copyright (c) 2011 The Snappy-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. + +---------------------------------------------------------------------- +License notice for github.com/konsorten/go-windows-terminal-sequences +---------------------------------------------------------------------- + +(The MIT License) + +Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de) + +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. + +---------------------------------------------------------------------- +License notice for github.com/kr/pretty +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright 2012 Keith Rarick + +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. + +---------------------------------------------------------------------- +License notice for github.com/kr/text +---------------------------------------------------------------------- + +Copyright 2012 Keith Rarick + +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. + +---------------------------------------------------------------------- +License notice for github.com/markbates/oncer +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Mark Bates + +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. + +---------------------------------------------------------------------- +License notice for github.com/markbates/safe +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Mark Bates + +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. + +---------------------------------------------------------------------- +License notice for github.com/montanaflynn/stats +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2014-2015 Montana Flynn (https://anonfunction.com) + +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. + +---------------------------------------------------------------------- +License notice for github.com/pkg/errors +---------------------------------------------------------------------- + +Copyright (c) 2015, Dave Cheney +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. + +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. + +---------------------------------------------------------------------- +License notice for github.com/pmezard/go-difflib +---------------------------------------------------------------------- + +Copyright (c) 2013, Patrick Mezard +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. + The names of its contributors may not 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 +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. + +---------------------------------------------------------------------- +License notice for github.com/rogpeppe/go-internal +---------------------------------------------------------------------- + +Copyright (c) 2018 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. + +---------------------------------------------------------------------- +License notice for github.com/stretchr/testify +---------------------------------------------------------------------- + +MIT License + +Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. + +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. + +---------------------------------------------------------------------- +License notice for github.com/tidwall/pretty +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2017 Josh Baker + +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. + +---------------------------------------------------------------------- +License notice for github.com/xdg-go/pbkdf2 +---------------------------------------------------------------------- + + + 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. + +---------------------------------------------------------------------- +License notice for github.com/xdg-go/scram +---------------------------------------------------------------------- + + + 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. + +---------------------------------------------------------------------- +License notice for github.com/xdg-go/stringprep +---------------------------------------------------------------------- + + + 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. + +---------------------------------------------------------------------- +License notice for github.com/youmark/pkcs8 +---------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2014 youmark + +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. +---------------------------------------------------------------------- +License notice for golang.org/x/crypto +---------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for golang.org/x/sync +---------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for golang.org/x/sys +---------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for golang.org/x/text +---------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for golang.org/x/tools +---------------------------------------------------------------------- + +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. + +---------------------------------------------------------------------- +License notice for golang.org/x/xerrors +---------------------------------------------------------------------- + +Copyright (c) 2019 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. + +---------------------------------------------------------------------- +License notice for gopkg.in/yaml.v3 +---------------------------------------------------------------------- + + +This project is covered by two different licenses: MIT and Apache. + +#### MIT License #### + +The following files were ported to Go from C files of libyaml, and thus +are still covered by their original MIT license, with the additional +copyright staring in 2011 when the project was ported over: + + apic.go emitterc.go parserc.go readerc.go scannerc.go + writerc.go yamlh.go yamlprivateh.go + +Copyright (c) 2006-2010 Kirill Simonov +Copyright (c) 2006-2011 Kirill Simonov + +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. + +### Apache License ### + +All the remaining project files are covered by the Apache license: + +Copyright (c) 2011-2019 Canonical Ltd + +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/mongo/bson/benchmark_test.go b/mongo/bson/benchmark_test.go new file mode 100644 index 0000000..a219051 --- /dev/null +++ b/mongo/bson/benchmark_test.go @@ -0,0 +1,307 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bson + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "testing" +) + +type encodetest struct { + Field1String string + Field1Int64 int64 + Field1Float64 float64 + Field2String string + Field2Int64 int64 + Field2Float64 float64 + Field3String string + Field3Int64 int64 + Field3Float64 float64 + Field4String string + Field4Int64 int64 + Field4Float64 float64 +} + +type nestedtest1 struct { + Nested nestedtest2 +} + +type nestedtest2 struct { + Nested nestedtest3 +} + +type nestedtest3 struct { + Nested nestedtest4 +} + +type nestedtest4 struct { + Nested nestedtest5 +} + +type nestedtest5 struct { + Nested nestedtest6 +} + +type nestedtest6 struct { + Nested nestedtest7 +} + +type nestedtest7 struct { + Nested nestedtest8 +} + +type nestedtest8 struct { + Nested nestedtest9 +} + +type nestedtest9 struct { + Nested nestedtest10 +} + +type nestedtest10 struct { + Nested nestedtest11 +} + +type nestedtest11 struct { + Nested encodetest +} + +var encodetestInstance = encodetest{ + Field1String: "foo", + Field1Int64: 1, + Field1Float64: 3.0, + Field2String: "bar", + Field2Int64: 2, + Field2Float64: 3.1, + Field3String: "baz", + Field3Int64: 3, + Field3Float64: 3.14, + Field4String: "qux", + Field4Int64: 4, + Field4Float64: 3.141, +} + +var nestedInstance = nestedtest1{ + nestedtest2{ + nestedtest3{ + nestedtest4{ + nestedtest5{ + nestedtest6{ + nestedtest7{ + nestedtest8{ + nestedtest9{ + nestedtest10{ + nestedtest11{ + encodetest{ + Field1String: "foo", + Field1Int64: 1, + Field1Float64: 3.0, + Field2String: "bar", + Field2Int64: 2, + Field2Float64: 3.1, + Field3String: "baz", + Field3Int64: 3, + Field3Float64: 3.14, + Field4String: "qux", + Field4Int64: 4, + Field4Float64: 3.141, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} + +const extendedBSONDir = "../testdata/extended_bson" + +// readExtJSONFile reads the GZIP-compressed extended JSON document from the given filename in the +// "extended BSON" test data directory (../testdata/extended_bson) and returns it as a +// map[string]interface{}. It panics on any errors. +func readExtJSONFile(filename string) map[string]interface{} { + filePath := path.Join(extendedBSONDir, filename) + file, err := os.Open(filePath) + if err != nil { + panic(fmt.Sprintf("error opening file %q: %s", filePath, err)) + } + defer func() { + _ = file.Close() + }() + + gz, err := gzip.NewReader(file) + if err != nil { + panic(fmt.Sprintf("error creating GZIP reader: %s", err)) + } + defer func() { + _ = gz.Close() + }() + + data, err := ioutil.ReadAll(gz) + if err != nil { + panic(fmt.Sprintf("error reading GZIP contents of file: %s", err)) + } + + var v map[string]interface{} + err = UnmarshalExtJSON(data, false, &v) + if err != nil { + panic(fmt.Sprintf("error unmarshalling extended JSON: %s", err)) + } + + return v +} + +func BenchmarkMarshal(b *testing.B) { + cases := []struct { + desc string + value interface{} + }{ + { + desc: "simple struct", + value: encodetestInstance, + }, + { + desc: "nested struct", + value: nestedInstance, + }, + { + desc: "deep_bson.json.gz", + value: readExtJSONFile("deep_bson.json.gz"), + }, + { + desc: "flat_bson.json.gz", + value: readExtJSONFile("flat_bson.json.gz"), + }, + { + desc: "full_bson.json.gz", + value: readExtJSONFile("full_bson.json.gz"), + }, + } + + for _, tc := range cases { + b.Run(tc.desc, func(b *testing.B) { + b.Run("BSON", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := Marshal(tc.value) + if err != nil { + b.Errorf("error marshalling BSON: %s", err) + } + } + }) + + b.Run("extJSON", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := MarshalExtJSON(tc.value, true, false) + if err != nil { + b.Errorf("error marshalling extended JSON: %s", err) + } + } + }) + + b.Run("JSON", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := json.Marshal(tc.value) + if err != nil { + b.Errorf("error marshalling JSON: %s", err) + } + } + }) + }) + } +} + +func BenchmarkUnmarshal(b *testing.B) { + cases := []struct { + desc string + value interface{} + }{ + { + desc: "simple struct", + value: encodetestInstance, + }, + { + desc: "nested struct", + value: nestedInstance, + }, + { + desc: "deep_bson.json.gz", + value: readExtJSONFile("deep_bson.json.gz"), + }, + { + desc: "flat_bson.json.gz", + value: readExtJSONFile("flat_bson.json.gz"), + }, + { + desc: "full_bson.json.gz", + value: readExtJSONFile("full_bson.json.gz"), + }, + } + + for _, tc := range cases { + b.Run(tc.desc, func(b *testing.B) { + b.Run("BSON", func(b *testing.B) { + data, err := Marshal(tc.value) + if err != nil { + b.Errorf("error marshalling BSON: %s", err) + return + } + + b.ResetTimer() + var v2 map[string]interface{} + for i := 0; i < b.N; i++ { + err := Unmarshal(data, &v2) + if err != nil { + b.Errorf("error unmarshalling BSON: %s", err) + } + } + }) + + b.Run("extJSON", func(b *testing.B) { + data, err := MarshalExtJSON(tc.value, true, false) + if err != nil { + b.Errorf("error marshalling extended JSON: %s", err) + return + } + + b.ResetTimer() + var v2 map[string]interface{} + for i := 0; i < b.N; i++ { + err := UnmarshalExtJSON(data, true, &v2) + if err != nil { + b.Errorf("error unmarshalling extended JSON: %s", err) + } + } + }) + + b.Run("JSON", func(b *testing.B) { + data, err := json.Marshal(tc.value) + if err != nil { + b.Errorf("error marshalling JSON: %s", err) + return + } + + b.ResetTimer() + var v2 map[string]interface{} + for i := 0; i < b.N; i++ { + err := json.Unmarshal(data, &v2) + if err != nil { + b.Errorf("error unmarshalling JSON: %s", err) + } + } + }) + }) + } +} diff --git a/mongo/bson/bson.go b/mongo/bson/bson.go new file mode 100644 index 0000000..a0d8185 --- /dev/null +++ b/mongo/bson/bson.go @@ -0,0 +1,50 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 +// +// Based on gopkg.in/mgo.v2/bson by Gustavo Niemeyer +// See THIRD-PARTY-NOTICES for original license terms. + +package bson // import "go.mongodb.org/mongo-driver/bson" + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Zeroer allows custom struct types to implement a report of zero +// state. All struct types that don't implement Zeroer or where IsZero +// returns false are considered to be not zero. +type Zeroer interface { + IsZero() bool +} + +// D is an ordered representation of a BSON document. This type should be used when the order of the elements matters, +// such as MongoDB command documents. If the order of the elements does not matter, an M should be used instead. +// +// A D should not be constructed with duplicate key names, as that can cause undefined server behavior. +// +// Example usage: +// +// bson.D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}} +type D = primitive.D + +// E represents a BSON element for a D. It is usually used inside a D. +type E = primitive.E + +// M is an unordered representation of a BSON document. This type should be used when the order of the elements does not +// matter. This type is handled as a regular map[string]interface{} when encoding and decoding. Elements will be +// serialized in an undefined, random order. If the order of the elements matters, a D should be used instead. +// +// Example usage: +// +// bson.M{"foo": "bar", "hello": "world", "pi": 3.14159} +type M = primitive.M + +// An A is an ordered representation of a BSON array. +// +// Example usage: +// +// bson.A{"bar", "world", 3.14159, bson.D{{"qux", 12345}}} +type A = primitive.A diff --git a/mongo/bson/bson_corpus_spec_test.go b/mongo/bson/bson_corpus_spec_test.go new file mode 100644 index 0000000..66378be --- /dev/null +++ b/mongo/bson/bson_corpus_spec_test.go @@ -0,0 +1,530 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bson + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "math" + "os" + "path" + "strconv" + "strings" + "testing" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "github.com/tidwall/pretty" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/internal/testutil/assert" +) + +type testCase struct { + Description string `json:"description"` + BsonType string `json:"bson_type"` + TestKey *string `json:"test_key"` + Valid []validityTestCase `json:"valid"` + DecodeErrors []decodeErrorTestCase `json:"decodeErrors"` + ParseErrors []parseErrorTestCase `json:"parseErrors"` + Deprecated *bool `json:"deprecated"` +} + +type validityTestCase struct { + Description string `json:"description"` + CanonicalBson string `json:"canonical_bson"` + CanonicalExtJSON string `json:"canonical_extjson"` + RelaxedExtJSON *string `json:"relaxed_extjson"` + DegenerateBSON *string `json:"degenerate_bson"` + DegenerateExtJSON *string `json:"degenerate_extjson"` + ConvertedBSON *string `json:"converted_bson"` + ConvertedExtJSON *string `json:"converted_extjson"` + Lossy *bool `json:"lossy"` +} + +type decodeErrorTestCase struct { + Description string `json:"description"` + Bson string `json:"bson"` +} + +type parseErrorTestCase struct { + Description string `json:"description"` + String string `json:"string"` +} + +const dataDir = "../testdata/bson-corpus/" + +func findJSONFilesInDir(dir string) ([]string, error) { + files := make([]string, 0) + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() || path.Ext(entry.Name()) != ".json" { + continue + } + + files = append(files, entry.Name()) + } + + return files, nil +} + +// seedExtJSON will add the byte representation of the "extJSON" string to the fuzzer's coprus. +func seedExtJSON(f *testing.F, extJSON string, extJSONType string, desc string) { + jbytes, err := jsonToBytes(extJSON, extJSONType, desc) + if err != nil { + f.Fatalf("failed to convert JSON to bytes: %v", err) + } + + f.Add(jbytes) +} + +// seedTestCase will add the byte representation for each "extJSON" string of each valid test case to the fuzzer's +// corpus. +func seedTestCase(f *testing.F, tcase *testCase) { + for _, vtc := range tcase.Valid { + seedExtJSON(f, vtc.CanonicalExtJSON, "canonical", vtc.Description) + + // Seed the relaxed extended JSON. + if vtc.RelaxedExtJSON != nil { + seedExtJSON(f, *vtc.RelaxedExtJSON, "relaxed", vtc.Description) + } + + // Seed the degenerate extended JSON. + if vtc.DegenerateExtJSON != nil { + seedExtJSON(f, *vtc.DegenerateExtJSON, "degenerate", vtc.Description) + } + + // Seed the converted extended JSON. + if vtc.ConvertedExtJSON != nil { + seedExtJSON(f, *vtc.ConvertedExtJSON, "converted", vtc.Description) + } + } +} + +// seedBSONCorpus will unmarshal the data from "testdata/bson-corpus" into a slice of "testCase" structs and then +// marshal the "*_extjson" field of each "validityTestCase" into a slice of bytes to seed the fuzz corpus. +func seedBSONCorpus(f *testing.F) { + fileNames, err := findJSONFilesInDir(dataDir) + if err != nil { + f.Fatalf("failed to find JSON files in directory %q: %v", dataDir, err) + } + + for _, fileName := range fileNames { + filePath := path.Join(dataDir, fileName) + + file, err := os.Open(filePath) + if err != nil { + f.Fatalf("failed to open file %q: %v", filePath, err) + } + + var tcase testCase + if err := json.NewDecoder(file).Decode(&tcase); err != nil { + f.Fatal(err) + } + + seedTestCase(f, &tcase) + } +} + +func needsEscapedUnicode(bsonType string) bool { + return bsonType == "0x02" || bsonType == "0x0D" || bsonType == "0x0E" || bsonType == "0x0F" +} + +func unescapeUnicode(s, bsonType string) string { + if !needsEscapedUnicode(bsonType) { + return s + } + + newS := "" + + for i := 0; i < len(s); i++ { + c := s[i] + switch c { + case '\\': + switch s[i+1] { + case 'u': + us := s[i : i+6] + u, err := strconv.Unquote(strings.Replace(strconv.Quote(us), `\\u`, `\u`, 1)) + if err != nil { + return "" + } + for _, r := range u { + if r < ' ' { + newS += fmt.Sprintf(`\u%04x`, r) + } else { + newS += string(r) + } + } + i += 5 + default: + newS += string(c) + } + default: + if c > unicode.MaxASCII { + r, size := utf8.DecodeRune([]byte(s[i:])) + newS += string(r) + i += size - 1 + } else { + newS += string(c) + } + } + } + + return newS +} + +func formatDouble(f float64) string { + var s string + if math.IsInf(f, 1) { + s = "Infinity" + } else if math.IsInf(f, -1) { + s = "-Infinity" + } else if math.IsNaN(f) { + s = "NaN" + } else { + // Print exactly one decimalType place for integers; otherwise, print as many are necessary to + // perfectly represent it. + s = strconv.FormatFloat(f, 'G', -1, 64) + if !strings.ContainsRune(s, 'E') && !strings.ContainsRune(s, '.') { + s += ".0" + } + } + + return s +} + +func normalizeCanonicalDouble(t *testing.T, key string, cEJ string) string { + // Unmarshal string into map + cEJMap := make(map[string]map[string]string) + err := json.Unmarshal([]byte(cEJ), &cEJMap) + require.NoError(t, err) + + // Parse the float contained by the map. + expectedString := cEJMap[key]["$numberDouble"] + expectedFloat, err := strconv.ParseFloat(expectedString, 64) + require.NoError(t, err) + + // Normalize the string + return fmt.Sprintf(`{"%s":{"$numberDouble":"%s"}}`, key, formatDouble(expectedFloat)) +} + +func normalizeRelaxedDouble(t *testing.T, key string, rEJ string) string { + // Unmarshal string into map + rEJMap := make(map[string]float64) + err := json.Unmarshal([]byte(rEJ), &rEJMap) + if err != nil { + return normalizeCanonicalDouble(t, key, rEJ) + } + + // Parse the float contained by the map. + expectedFloat := rEJMap[key] + + // Normalize the string + return fmt.Sprintf(`{"%s":%s}`, key, formatDouble(expectedFloat)) +} + +// bsonToNative decodes the BSON bytes (b) into a native Document +func bsonToNative(t *testing.T, b []byte, bType, testDesc string) D { + var doc D + err := Unmarshal(b, &doc) + expectNoError(t, err, fmt.Sprintf("%s: decoding %s BSON", testDesc, bType)) + return doc +} + +// nativeToBSON encodes the native Document (doc) into canonical BSON and compares it to the expected +// canonical BSON (cB) +func nativeToBSON(t *testing.T, cB []byte, doc D, testDesc, bType, docSrcDesc string) { + actual, err := Marshal(doc) + expectNoError(t, err, fmt.Sprintf("%s: encoding %s BSON", testDesc, bType)) + + if diff := cmp.Diff(cB, actual); diff != "" { + t.Errorf("%s: 'native_to_bson(%s) = cB' failed (-want, +got):\n-%v\n+%v\n", + testDesc, docSrcDesc, cB, actual) + t.FailNow() + } +} + +// jsonToNative decodes the extended JSON string (ej) into a native Document +func jsonToNative(ej, ejType, testDesc string) (D, error) { + var doc D + if err := UnmarshalExtJSON([]byte(ej), ejType != "relaxed", &doc); err != nil { + return nil, fmt.Errorf("%s: decoding %s extended JSON: %w", testDesc, ejType, err) + } + return doc, nil +} + +// jsonToBytes decodes the extended JSON string (ej) into canonical BSON and then encodes it into a byte slice. +func jsonToBytes(ej, ejType, testDesc string) ([]byte, error) { + native, err := jsonToNative(ej, ejType, testDesc) + if err != nil { + return nil, err + } + + b, err := Marshal(native) + if err != nil { + return nil, fmt.Errorf("%s: encoding %s BSON: %w", testDesc, ejType, err) + } + + return b, nil +} + +// nativeToJSON encodes the native Document (doc) into an extended JSON string +func nativeToJSON(t *testing.T, ej string, doc D, testDesc, ejType, ejShortName, docSrcDesc string) { + actualEJ, err := MarshalExtJSON(doc, ejType != "relaxed", true) + expectNoError(t, err, fmt.Sprintf("%s: encoding %s extended JSON", testDesc, ejType)) + + if diff := cmp.Diff(ej, string(actualEJ)); diff != "" { + t.Errorf("%s: 'native_to_%s_extended_json(%s) = %s' failed (-want, +got):\n%s\n", + testDesc, ejType, docSrcDesc, ejShortName, diff) + t.FailNow() + } +} + +func runTest(t *testing.T, file string) { + filepath := path.Join(dataDir, file) + content, err := os.ReadFile(filepath) + require.NoError(t, err) + + // Remove ".json" from filename. + file = file[:len(file)-5] + testName := "bson_corpus--" + file + + t.Run(testName, func(t *testing.T) { + var test testCase + require.NoError(t, json.Unmarshal(content, &test)) + + t.Run("valid", func(t *testing.T) { + for _, v := range test.Valid { + t.Run(v.Description, func(t *testing.T) { + // get canonical BSON + cB, err := hex.DecodeString(v.CanonicalBson) + expectNoError(t, err, fmt.Sprintf("%s: reading canonical BSON", v.Description)) + + // get canonical extended JSON + cEJ := unescapeUnicode(string(pretty.Ugly([]byte(v.CanonicalExtJSON))), test.BsonType) + if test.BsonType == "0x01" { + cEJ = normalizeCanonicalDouble(t, *test.TestKey, cEJ) + } + + /*** canonical BSON round-trip tests ***/ + doc := bsonToNative(t, cB, "canonical", v.Description) + + // native_to_bson(bson_to_native(cB)) = cB + nativeToBSON(t, cB, doc, v.Description, "canonical", "bson_to_native(cB)") + + // native_to_canonical_extended_json(bson_to_native(cB)) = cEJ + nativeToJSON(t, cEJ, doc, v.Description, "canonical", "cEJ", "bson_to_native(cB)") + + // native_to_relaxed_extended_json(bson_to_native(cB)) = rEJ (if rEJ exists) + if v.RelaxedExtJSON != nil { + rEJ := unescapeUnicode(string(pretty.Ugly([]byte(*v.RelaxedExtJSON))), test.BsonType) + if test.BsonType == "0x01" { + rEJ = normalizeRelaxedDouble(t, *test.TestKey, rEJ) + } + + nativeToJSON(t, rEJ, doc, v.Description, "relaxed", "rEJ", "bson_to_native(cB)") + + /*** relaxed extended JSON round-trip tests (if exists) ***/ + doc, err = jsonToNative(rEJ, "relaxed", v.Description) + require.NoError(t, err) + + // native_to_relaxed_extended_json(json_to_native(rEJ)) = rEJ + nativeToJSON(t, rEJ, doc, v.Description, "relaxed", "eJR", "json_to_native(rEJ)") + } + + /*** canonical extended JSON round-trip tests ***/ + doc, err = jsonToNative(cEJ, "canonical", v.Description) + require.NoError(t, err) + + // native_to_canonical_extended_json(json_to_native(cEJ)) = cEJ + nativeToJSON(t, cEJ, doc, v.Description, "canonical", "cEJ", "json_to_native(cEJ)") + + // native_to_bson(json_to_native(cEJ)) = cb (unless lossy) + if v.Lossy == nil || !*v.Lossy { + nativeToBSON(t, cB, doc, v.Description, "canonical", "json_to_native(cEJ)") + } + + /*** degenerate BSON round-trip tests (if exists) ***/ + if v.DegenerateBSON != nil { + dB, err := hex.DecodeString(*v.DegenerateBSON) + expectNoError(t, err, fmt.Sprintf("%s: reading degenerate BSON", v.Description)) + + doc = bsonToNative(t, dB, "degenerate", v.Description) + + // native_to_bson(bson_to_native(dB)) = cB + nativeToBSON(t, cB, doc, v.Description, "degenerate", "bson_to_native(dB)") + } + + /*** degenerate JSON round-trip tests (if exists) ***/ + if v.DegenerateExtJSON != nil { + dEJ := unescapeUnicode(string(pretty.Ugly([]byte(*v.DegenerateExtJSON))), test.BsonType) + if test.BsonType == "0x01" { + dEJ = normalizeCanonicalDouble(t, *test.TestKey, dEJ) + } + + doc, err = jsonToNative(dEJ, "degenerate canonical", v.Description) + require.NoError(t, err) + + // native_to_canonical_extended_json(json_to_native(dEJ)) = cEJ + nativeToJSON(t, cEJ, doc, v.Description, "degenerate canonical", "cEJ", "json_to_native(dEJ)") + + // native_to_bson(json_to_native(dEJ)) = cB (unless lossy) + if v.Lossy == nil || !*v.Lossy { + nativeToBSON(t, cB, doc, v.Description, "canonical", "json_to_native(dEJ)") + } + } + }) + } + }) + + t.Run("decode error", func(t *testing.T) { + for _, d := range test.DecodeErrors { + t.Run(d.Description, func(t *testing.T) { + b, err := hex.DecodeString(d.Bson) + expectNoError(t, err, d.Description) + + var doc D + err = Unmarshal(b, &doc) + + // The driver unmarshals invalid UTF-8 strings without error. Loop over the unmarshalled elements + // and assert that there was no error if any of the string or DBPointer values contain invalid UTF-8 + // characters. + for _, elem := range doc { + str, ok := elem.Value.(string) + invalidString := ok && !utf8.ValidString(str) + dbPtr, ok := elem.Value.(primitive.DBPointer) + invalidDBPtr := ok && !utf8.ValidString(dbPtr.DB) + + if invalidString || invalidDBPtr { + expectNoError(t, err, d.Description) + return + } + } + + expectError(t, err, fmt.Sprintf("%s: expected decode error", d.Description)) + }) + } + }) + + t.Run("parse error", func(t *testing.T) { + for _, p := range test.ParseErrors { + t.Run(p.Description, func(t *testing.T) { + s := unescapeUnicode(p.String, test.BsonType) + if test.BsonType == "0x13" { + s = fmt.Sprintf(`{"decimal128": {"$numberDecimal": "%s"}}`, s) + } + + switch test.BsonType { + case "0x00", "0x05", "0x13": + var doc D + err := UnmarshalExtJSON([]byte(s), true, &doc) + // Null bytes are validated when marshaling to BSON + if strings.Contains(p.Description, "Null") { + _, err = Marshal(doc) + } + expectError(t, err, fmt.Sprintf("%s: expected parse error", p.Description)) + default: + t.Errorf("Update test to check for parse errors for type %s", test.BsonType) + t.Fail() + } + }) + } + }) + }) +} + +func Test_BsonCorpus(t *testing.T) { + jsonFiles, err := findJSONFilesInDir(dataDir) + if err != nil { + t.Fatalf("error finding JSON files in %s: %v", dataDir, err) + } + + for _, file := range jsonFiles { + runTest(t, file) + } +} + +func expectNoError(t *testing.T, err error, desc string) { + if err != nil { + t.Helper() + t.Errorf("%s: Unepexted error: %v", desc, err) + t.FailNow() + } +} + +func expectError(t *testing.T, err error, desc string) { + if err == nil { + t.Helper() + t.Errorf("%s: Expected error", desc) + t.FailNow() + } +} + +func TestRelaxedUUIDValidation(t *testing.T) { + testCases := []struct { + description string + canonicalExtJSON string + degenerateExtJSON string + expectedErr string + }{ + { + "valid uuid", + "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"04\"}}}", + "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4\"}}", + "", + }, + { + "invalid uuid--no hyphens", + "", + "{\"x\" : { \"$uuid\" : \"73ffd26444b34c6990e8e7d1dfc035d4\"}}", + "$uuid value does not follow RFC 4122 format regarding length and hyphens", + }, + { + "invalid uuid--trailing hyphens", + "", + "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035--\"}}", + "$uuid value does not follow RFC 4122 format regarding length and hyphens", + }, + { + "invalid uuid--malformed hex", + "", + "{\"x\" : { \"$uuid\" : \"q3@fd26l-44b3-4c69-90e8-e7d1dfc035d4\"}}", + "$uuid value does not follow RFC 4122 format regarding hex bytes: encoding/hex: invalid byte: U+0071 'q'", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + // get canonical extended JSON + cEJ := unescapeUnicode(string(pretty.Ugly([]byte(tc.canonicalExtJSON))), "0x05") + + // get degenerate extended JSON + dEJ := unescapeUnicode(string(pretty.Ugly([]byte(tc.degenerateExtJSON))), "0x05") + + // convert dEJ to native doc + var doc D + err := UnmarshalExtJSON([]byte(dEJ), true, &doc) + + if tc.expectedErr != "" { + assert.Equal(t, tc.expectedErr, err.Error(), "expected error %v, got %v", tc.expectedErr, err) + } else { + assert.Nil(t, err, "expected no error, got error: %v", err) + + // Marshal doc into extended JSON and compare with cEJ + nativeToJSON(t, cEJ, doc, tc.description, "degenerate canonical", "cEJ", "json_to_native(dEJ)") + } + }) + } + +} diff --git a/mongo/bson/bson_test.go b/mongo/bson/bson_test.go new file mode 100644 index 0000000..0e8a3ce --- /dev/null +++ b/mongo/bson/bson_test.go @@ -0,0 +1,279 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bson + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsoncodec" + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/internal/testutil/assert" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +func noerr(t *testing.T, err error) { + if err != nil { + t.Helper() + t.Errorf("Unexpected error: (%T)%v", err, err) + t.FailNow() + } +} + +func TestTimeRoundTrip(t *testing.T) { + val := struct { + Value time.Time + ID string + }{ + ID: "time-rt-test", + } + + if !val.Value.IsZero() { + t.Errorf("Did not get zero time as expected.") + } + + bsonOut, err := Marshal(val) + noerr(t, err) + rtval := struct { + Value time.Time + ID string + }{} + + err = Unmarshal(bsonOut, &rtval) + noerr(t, err) + if !cmp.Equal(val, rtval) { + t.Errorf("Did not round trip properly. got %v; want %v", val, rtval) + } + if !rtval.Value.IsZero() { + t.Errorf("Did not get zero time as expected.") + } +} + +func TestNonNullTimeRoundTrip(t *testing.T) { + now := time.Now() + now = time.Unix(now.Unix(), 0) + val := struct { + Value time.Time + ID string + }{ + ID: "time-rt-test", + Value: now, + } + + bsonOut, err := Marshal(val) + noerr(t, err) + rtval := struct { + Value time.Time + ID string + }{} + + err = Unmarshal(bsonOut, &rtval) + noerr(t, err) + if !cmp.Equal(val, rtval) { + t.Errorf("Did not round trip properly. got %v; want %v", val, rtval) + } +} + +func TestD(t *testing.T) { + t.Run("can marshal", func(t *testing.T) { + d := D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}} + idx, want := bsoncore.AppendDocumentStart(nil) + want = bsoncore.AppendStringElement(want, "foo", "bar") + want = bsoncore.AppendStringElement(want, "hello", "world") + want = bsoncore.AppendDoubleElement(want, "pi", 3.14159) + want, err := bsoncore.AppendDocumentEnd(want, idx) + noerr(t, err) + got, err := Marshal(d) + noerr(t, err) + if !bytes.Equal(got, want) { + t.Errorf("Marshaled documents do not match. got %v; want %v", Raw(got), Raw(want)) + } + }) + t.Run("can unmarshal", func(t *testing.T) { + want := D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}} + idx, doc := bsoncore.AppendDocumentStart(nil) + doc = bsoncore.AppendStringElement(doc, "foo", "bar") + doc = bsoncore.AppendStringElement(doc, "hello", "world") + doc = bsoncore.AppendDoubleElement(doc, "pi", 3.14159) + doc, err := bsoncore.AppendDocumentEnd(doc, idx) + noerr(t, err) + var got D + err = Unmarshal(doc, &got) + noerr(t, err) + if !cmp.Equal(got, want) { + t.Errorf("Unmarshaled documents do not match. got %v; want %v", got, want) + } + }) +} + +type stringerString string + +func (ss stringerString) String() string { + return "bar" +} + +type keyBool bool + +func (kb keyBool) MarshalKey() (string, error) { + return fmt.Sprintf("%v", kb), nil +} + +func (kb *keyBool) UnmarshalKey(key string) error { + switch key { + case "true": + *kb = true + case "false": + *kb = false + default: + return fmt.Errorf("invalid bool value %v", key) + } + return nil +} + +type keyStruct struct { + val int64 +} + +func (k keyStruct) MarshalText() (text []byte, err error) { + str := strconv.FormatInt(k.val, 10) + + return []byte(str), nil +} + +func (k *keyStruct) UnmarshalText(text []byte) error { + val, err := strconv.ParseInt(string(text), 10, 64) + if err != nil { + return err + } + + *k = keyStruct{ + val: val, + } + + return nil +} + +func TestMapCodec(t *testing.T) { + t.Run("EncodeKeysWithStringer", func(t *testing.T) { + strstr := stringerString("foo") + mapObj := map[stringerString]int{strstr: 1} + testCases := []struct { + name string + opts *bsonoptions.MapCodecOptions + key string + }{ + {"default", bsonoptions.MapCodec(), "foo"}, + {"true", bsonoptions.MapCodec().SetEncodeKeysWithStringer(true), "bar"}, + {"false", bsonoptions.MapCodec().SetEncodeKeysWithStringer(false), "foo"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mapCodec := bsoncodec.NewMapCodec(tc.opts) + mapRegistry := NewRegistryBuilder().RegisterDefaultEncoder(reflect.Map, mapCodec).Build() + val, err := MarshalWithRegistry(mapRegistry, mapObj) + assert.Nil(t, err, "Marshal error: %v", err) + assert.True(t, strings.Contains(string(val), tc.key), "expected result to contain %v, got: %v", tc.key, string(val)) + }) + } + }) + + t.Run("keys implements keyMarshaler and keyUnmarshaler", func(t *testing.T) { + mapObj := map[keyBool]int{keyBool(true): 1} + + doc, err := Marshal(mapObj) + assert.Nil(t, err, "Marshal error: %v", err) + idx, want := bsoncore.AppendDocumentStart(nil) + want = bsoncore.AppendInt32Element(want, "true", 1) + want, _ = bsoncore.AppendDocumentEnd(want, idx) + assert.Equal(t, want, doc, "expected result %v, got %v", string(want), string(doc)) + + var got map[keyBool]int + err = Unmarshal(doc, &got) + assert.Nil(t, err, "Unmarshal error: %v", err) + assert.Equal(t, mapObj, got, "expected result %v, got %v", mapObj, got) + + }) + + t.Run("keys implements encoding.TextMarshaler and encoding.TextUnmarshaler", func(t *testing.T) { + mapObj := map[keyStruct]int{ + {val: 10}: 100, + } + + doc, err := Marshal(mapObj) + assert.Nil(t, err, "Marshal error: %v", err) + idx, want := bsoncore.AppendDocumentStart(nil) + want = bsoncore.AppendInt32Element(want, "10", 100) + want, _ = bsoncore.AppendDocumentEnd(want, idx) + assert.Equal(t, want, doc, "expected result %v, got %v", string(want), string(doc)) + + var got map[keyStruct]int + err = Unmarshal(doc, &got) + assert.Nil(t, err, "Unmarshal error: %v", err) + assert.Equal(t, mapObj, got, "expected result %v, got %v", mapObj, got) + + }) +} + +func TestExtJSONEscapeKey(t *testing.T) { + doc := D{{Key: "\\usb#", Value: int32(1)}} + b, err := MarshalExtJSON(&doc, false, false) + noerr(t, err) + + want := "{\"\\\\usb#\":1}" + if diff := cmp.Diff(want, string(b)); diff != "" { + t.Errorf("Marshaled documents do not match. got %v, want %v", string(b), want) + } + + var got D + err = UnmarshalExtJSON(b, false, &got) + noerr(t, err) + if !cmp.Equal(got, doc) { + t.Errorf("Unmarshaled documents do not match. got %v; want %v", got, doc) + } +} + +func TestBsoncoreArray(t *testing.T) { + type BSONDocumentArray struct { + Array []D `bson:"array"` + } + + type BSONArray struct { + Array bsoncore.Array `bson:"array"` + } + + bda := BSONDocumentArray{ + Array: []D{ + {{"x", 1}}, + {{"x", 2}}, + {{"x", 3}}, + }, + } + + expectedBSON, err := Marshal(bda) + assert.Nil(t, err, "Marshal bsoncore.Document array error: %v", err) + + var ba BSONArray + err = Unmarshal(expectedBSON, &ba) + assert.Nil(t, err, "Unmarshal error: %v", err) + + actualBSON, err := Marshal(ba) + assert.Nil(t, err, "Marshal bsoncore.Array error: %v", err) + + assert.Equal(t, expectedBSON, actualBSON, + "expected BSON to be %v after Marshalling again; got %v", expectedBSON, actualBSON) + + doc := bsoncore.Document(actualBSON) + v := doc.Lookup("array") + assert.Equal(t, bsontype.Array, v.Type, "expected type array, got %v", v.Type) +} diff --git a/mongo/bson/bsoncodec/array_codec.go b/mongo/bson/bsoncodec/array_codec.go new file mode 100644 index 0000000..4e24f9e --- /dev/null +++ b/mongo/bson/bsoncodec/array_codec.go @@ -0,0 +1,50 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +// ArrayCodec is the Codec used for bsoncore.Array values. +type ArrayCodec struct{} + +var defaultArrayCodec = NewArrayCodec() + +// NewArrayCodec returns an ArrayCodec. +func NewArrayCodec() *ArrayCodec { + return &ArrayCodec{} +} + +// EncodeValue is the ValueEncoder for bsoncore.Array values. +func (ac *ArrayCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tCoreArray { + return ValueEncoderError{Name: "CoreArrayEncodeValue", Types: []reflect.Type{tCoreArray}, Received: val} + } + + arr := val.Interface().(bsoncore.Array) + return bsonrw.Copier{}.CopyArrayFromBytes(vw, arr) +} + +// DecodeValue is the ValueDecoder for bsoncore.Array values. +func (ac *ArrayCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tCoreArray { + return ValueDecoderError{Name: "CoreArrayDecodeValue", Types: []reflect.Type{tCoreArray}, Received: val} + } + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, 0)) + } + + val.SetLen(0) + arr, err := bsonrw.Copier{}.AppendArrayBytes(val.Interface().(bsoncore.Array), vr) + val.Set(reflect.ValueOf(arr)) + return err +} diff --git a/mongo/bson/bsoncodec/bsoncodec.go b/mongo/bson/bsoncodec/bsoncodec.go new file mode 100644 index 0000000..098ed69 --- /dev/null +++ b/mongo/bson/bsoncodec/bsoncodec.go @@ -0,0 +1,238 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec // import "go.mongodb.org/mongo-driver/bson/bsoncodec" + +import ( + "fmt" + "reflect" + "strings" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + emptyValue = reflect.Value{} +) + +// Marshaler is an interface implemented by types that can marshal themselves +// into a BSON document represented as bytes. The bytes returned must be a valid +// BSON document if the error is nil. +type Marshaler interface { + MarshalBSON() ([]byte, error) +} + +// ValueMarshaler is an interface implemented by types that can marshal +// themselves into a BSON value as bytes. The type must be the valid type for +// the bytes returned. The bytes and byte type together must be valid if the +// error is nil. +type ValueMarshaler interface { + MarshalBSONValue() (bsontype.Type, []byte, error) +} + +// Unmarshaler is an interface implemented by types that can unmarshal a BSON +// document representation of themselves. The BSON bytes can be assumed to be +// valid. UnmarshalBSON must copy the BSON bytes if it wishes to retain the data +// after returning. +type Unmarshaler interface { + UnmarshalBSON([]byte) error +} + +// ValueUnmarshaler is an interface implemented by types that can unmarshal a +// BSON value representation of themselves. The BSON bytes and type can be +// assumed to be valid. UnmarshalBSONValue must copy the BSON value bytes if it +// wishes to retain the data after returning. +type ValueUnmarshaler interface { + UnmarshalBSONValue(bsontype.Type, []byte) error +} + +// ValueEncoderError is an error returned from a ValueEncoder when the provided value can't be +// encoded by the ValueEncoder. +type ValueEncoderError struct { + Name string + Types []reflect.Type + Kinds []reflect.Kind + Received reflect.Value +} + +func (vee ValueEncoderError) Error() string { + typeKinds := make([]string, 0, len(vee.Types)+len(vee.Kinds)) + for _, t := range vee.Types { + typeKinds = append(typeKinds, t.String()) + } + for _, k := range vee.Kinds { + if k == reflect.Map { + typeKinds = append(typeKinds, "map[string]*") + continue + } + typeKinds = append(typeKinds, k.String()) + } + received := vee.Received.Kind().String() + if vee.Received.IsValid() { + received = vee.Received.Type().String() + } + return fmt.Sprintf("%s can only encode valid %s, but got %s", vee.Name, strings.Join(typeKinds, ", "), received) +} + +// ValueDecoderError is an error returned from a ValueDecoder when the provided value can't be +// decoded by the ValueDecoder. +type ValueDecoderError struct { + Name string + Types []reflect.Type + Kinds []reflect.Kind + Received reflect.Value +} + +func (vde ValueDecoderError) Error() string { + typeKinds := make([]string, 0, len(vde.Types)+len(vde.Kinds)) + for _, t := range vde.Types { + typeKinds = append(typeKinds, t.String()) + } + for _, k := range vde.Kinds { + if k == reflect.Map { + typeKinds = append(typeKinds, "map[string]*") + continue + } + typeKinds = append(typeKinds, k.String()) + } + received := vde.Received.Kind().String() + if vde.Received.IsValid() { + received = vde.Received.Type().String() + } + return fmt.Sprintf("%s can only decode valid and settable %s, but got %s", vde.Name, strings.Join(typeKinds, ", "), received) +} + +// EncodeContext is the contextual information required for a Codec to encode a +// value. +type EncodeContext struct { + *Registry + MinSize bool +} + +// DecodeContext is the contextual information required for a Codec to decode a +// value. +type DecodeContext struct { + *Registry + Truncate bool + + // Ancestor is the type of a containing document. This is mainly used to determine what type + // should be used when decoding an embedded document into an empty interface. For example, if + // Ancestor is a bson.M, BSON embedded document values being decoded into an empty interface + // will be decoded into a bson.M. + // + // Deprecated: Use DefaultDocumentM or DefaultDocumentD instead. + Ancestor reflect.Type + + // defaultDocumentType specifies the Go type to decode top-level and nested BSON documents into. In particular, the + // usage for this field is restricted to data typed as "interface{}" or "map[string]interface{}". If DocumentType is + // set to a type that a BSON document cannot be unmarshaled into (e.g. "string"), unmarshalling will result in an + // error. DocumentType overrides the Ancestor field. + defaultDocumentType reflect.Type +} + +// DefaultDocumentM will decode empty documents using the primitive.M type. This behavior is restricted to data typed as +// "interface{}" or "map[string]interface{}". +func (dc *DecodeContext) DefaultDocumentM() { + dc.defaultDocumentType = reflect.TypeOf(primitive.M{}) +} + +// DefaultDocumentD will decode empty documents using the primitive.D type. This behavior is restricted to data typed as +// "interface{}" or "map[string]interface{}". +func (dc *DecodeContext) DefaultDocumentD() { + dc.defaultDocumentType = reflect.TypeOf(primitive.D{}) +} + +// ValueCodec is the interface that groups the methods to encode and decode +// values. +type ValueCodec interface { + ValueEncoder + ValueDecoder +} + +// ValueEncoder is the interface implemented by types that can handle the encoding of a value. +type ValueEncoder interface { + EncodeValue(EncodeContext, bsonrw.ValueWriter, reflect.Value) error +} + +// ValueEncoderFunc is an adapter function that allows a function with the correct signature to be +// used as a ValueEncoder. +type ValueEncoderFunc func(EncodeContext, bsonrw.ValueWriter, reflect.Value) error + +// EncodeValue implements the ValueEncoder interface. +func (fn ValueEncoderFunc) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + return fn(ec, vw, val) +} + +// ValueDecoder is the interface implemented by types that can handle the decoding of a value. +type ValueDecoder interface { + DecodeValue(DecodeContext, bsonrw.ValueReader, reflect.Value) error +} + +// ValueDecoderFunc is an adapter function that allows a function with the correct signature to be +// used as a ValueDecoder. +type ValueDecoderFunc func(DecodeContext, bsonrw.ValueReader, reflect.Value) error + +// DecodeValue implements the ValueDecoder interface. +func (fn ValueDecoderFunc) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + return fn(dc, vr, val) +} + +// typeDecoder is the interface implemented by types that can handle the decoding of a value given its type. +type typeDecoder interface { + decodeType(DecodeContext, bsonrw.ValueReader, reflect.Type) (reflect.Value, error) +} + +// typeDecoderFunc is an adapter function that allows a function with the correct signature to be used as a typeDecoder. +type typeDecoderFunc func(DecodeContext, bsonrw.ValueReader, reflect.Type) (reflect.Value, error) + +func (fn typeDecoderFunc) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + return fn(dc, vr, t) +} + +// decodeAdapter allows two functions with the correct signatures to be used as both a ValueDecoder and typeDecoder. +type decodeAdapter struct { + ValueDecoderFunc + typeDecoderFunc +} + +var _ ValueDecoder = decodeAdapter{} +var _ typeDecoder = decodeAdapter{} + +// decodeTypeOrValue calls decoder.decodeType is decoder is a typeDecoder. Otherwise, it allocates a new element of type +// t and calls decoder.DecodeValue on it. +func decodeTypeOrValue(decoder ValueDecoder, dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + td, _ := decoder.(typeDecoder) + return decodeTypeOrValueWithInfo(decoder, td, dc, vr, t, true) +} + +func decodeTypeOrValueWithInfo(vd ValueDecoder, td typeDecoder, dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type, convert bool) (reflect.Value, error) { + if td != nil { + val, err := td.decodeType(dc, vr, t) + if err == nil && convert && val.Type() != t { + // This conversion step is necessary for slices and maps. If a user declares variables like: + // + // type myBool bool + // var m map[string]myBool + // + // and tries to decode BSON bytes into the map, the decoding will fail if this conversion is not present + // because we'll try to assign a value of type bool to one of type myBool. + val = val.Convert(t) + } + return val, err + } + + val := reflect.New(t).Elem() + err := vd.DecodeValue(dc, vr, val) + return val, err +} + +// CodecZeroer is the interface implemented by Codecs that can also determine if +// a value of the type that would be encoded is zero. +type CodecZeroer interface { + IsTypeZero(interface{}) bool +} diff --git a/mongo/bson/bsoncodec/bsoncodec_test.go b/mongo/bson/bsoncodec/bsoncodec_test.go new file mode 100644 index 0000000..d14baf7 --- /dev/null +++ b/mongo/bson/bsoncodec/bsoncodec_test.go @@ -0,0 +1,143 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "reflect" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func ExampleValueEncoder() { + var _ ValueEncoderFunc = func(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if val.Kind() != reflect.String { + return ValueEncoderError{Name: "StringEncodeValue", Kinds: []reflect.Kind{reflect.String}, Received: val} + } + + return vw.WriteString(val.String()) + } +} + +func ExampleValueDecoder() { + var _ ValueDecoderFunc = func(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.String { + return ValueDecoderError{Name: "StringDecodeValue", Kinds: []reflect.Kind{reflect.String}, Received: val} + } + + if vr.Type() != bsontype.String { + return fmt.Errorf("cannot decode %v into a string type", vr.Type()) + } + + str, err := vr.ReadString() + if err != nil { + return err + } + val.SetString(str) + return nil + } +} + +func noerr(t *testing.T, err error) { + if err != nil { + t.Helper() + t.Errorf("Unexpected error: (%T)%v", err, err) + t.FailNow() + } +} + +func compareTime(t1, t2 time.Time) bool { + if t1.Location() != t2.Location() { + return false + } + return t1.Equal(t2) +} + +func compareErrors(err1, err2 error) bool { + if err1 == nil && err2 == nil { + return true + } + + if err1 == nil || err2 == nil { + return false + } + + if err1.Error() != err2.Error() { + return false + } + + return true +} + +func compareDecimal128(d1, d2 primitive.Decimal128) bool { + d1H, d1L := d1.GetBytes() + d2H, d2L := d2.GetBytes() + + if d1H != d2H { + return false + } + + if d1L != d2L { + return false + } + + return true +} + +type noPrivateFields struct { + a string +} + +func compareNoPrivateFields(npf1, npf2 noPrivateFields) bool { + return npf1.a != npf2.a // We don't want these to be equal +} + +type zeroTest struct { + reportZero bool +} + +func (z zeroTest) IsZero() bool { return z.reportZero } + +func compareZeroTest(_, _ zeroTest) bool { return true } + +type nonZeroer struct { + value bool +} + +type llCodec struct { + t *testing.T + decodeval interface{} + encodeval interface{} + err error +} + +func (llc *llCodec) EncodeValue(_ EncodeContext, _ bsonrw.ValueWriter, i interface{}) error { + if llc.err != nil { + return llc.err + } + + llc.encodeval = i + return nil +} + +func (llc *llCodec) DecodeValue(_ DecodeContext, _ bsonrw.ValueReader, val reflect.Value) error { + if llc.err != nil { + return llc.err + } + + if !reflect.TypeOf(llc.decodeval).AssignableTo(val.Type()) { + llc.t.Errorf("decodeval must be assignable to val provided to DecodeValue, but is not. decodeval %T; val %T", llc.decodeval, val) + return nil + } + + val.Set(reflect.ValueOf(llc.decodeval)) + return nil +} diff --git a/mongo/bson/bsoncodec/byte_slice_codec.go b/mongo/bson/bsoncodec/byte_slice_codec.go new file mode 100644 index 0000000..5a916cc --- /dev/null +++ b/mongo/bson/bsoncodec/byte_slice_codec.go @@ -0,0 +1,111 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// ByteSliceCodec is the Codec used for []byte values. +type ByteSliceCodec struct { + EncodeNilAsEmpty bool +} + +var ( + defaultByteSliceCodec = NewByteSliceCodec() + + _ ValueCodec = defaultByteSliceCodec + _ typeDecoder = defaultByteSliceCodec +) + +// NewByteSliceCodec returns a StringCodec with options opts. +func NewByteSliceCodec(opts ...*bsonoptions.ByteSliceCodecOptions) *ByteSliceCodec { + byteSliceOpt := bsonoptions.MergeByteSliceCodecOptions(opts...) + codec := ByteSliceCodec{} + if byteSliceOpt.EncodeNilAsEmpty != nil { + codec.EncodeNilAsEmpty = *byteSliceOpt.EncodeNilAsEmpty + } + return &codec +} + +// EncodeValue is the ValueEncoder for []byte. +func (bsc *ByteSliceCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tByteSlice { + return ValueEncoderError{Name: "ByteSliceEncodeValue", Types: []reflect.Type{tByteSlice}, Received: val} + } + if val.IsNil() && !bsc.EncodeNilAsEmpty { + return vw.WriteNull() + } + return vw.WriteBinary(val.Interface().([]byte)) +} + +func (bsc *ByteSliceCodec) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tByteSlice { + return emptyValue, ValueDecoderError{ + Name: "ByteSliceDecodeValue", + Types: []reflect.Type{tByteSlice}, + Received: reflect.Zero(t), + } + } + + var data []byte + var err error + switch vrType := vr.Type(); vrType { + case bsontype.String: + str, err := vr.ReadString() + if err != nil { + return emptyValue, err + } + data = []byte(str) + case bsontype.Symbol: + sym, err := vr.ReadSymbol() + if err != nil { + return emptyValue, err + } + data = []byte(sym) + case bsontype.Binary: + var subtype byte + data, subtype, err = vr.ReadBinary() + if err != nil { + return emptyValue, err + } + if subtype != bsontype.BinaryGeneric && subtype != bsontype.BinaryBinaryOld { + return emptyValue, decodeBinaryError{subtype: subtype, typeName: "[]byte"} + } + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a []byte", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(data), nil +} + +// DecodeValue is the ValueDecoder for []byte. +func (bsc *ByteSliceCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tByteSlice { + return ValueDecoderError{Name: "ByteSliceDecodeValue", Types: []reflect.Type{tByteSlice}, Received: val} + } + + elem, err := bsc.decodeType(dc, vr, tByteSlice) + if err != nil { + return err + } + + val.Set(elem) + return nil +} diff --git a/mongo/bson/bsoncodec/cond_addr_codec.go b/mongo/bson/bsoncodec/cond_addr_codec.go new file mode 100644 index 0000000..cb8180f --- /dev/null +++ b/mongo/bson/bsoncodec/cond_addr_codec.go @@ -0,0 +1,63 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonrw" +) + +// condAddrEncoder is the encoder used when a pointer to the encoding value has an encoder. +type condAddrEncoder struct { + canAddrEnc ValueEncoder + elseEnc ValueEncoder +} + +var _ ValueEncoder = (*condAddrEncoder)(nil) + +// newCondAddrEncoder returns an condAddrEncoder. +func newCondAddrEncoder(canAddrEnc, elseEnc ValueEncoder) *condAddrEncoder { + encoder := condAddrEncoder{canAddrEnc: canAddrEnc, elseEnc: elseEnc} + return &encoder +} + +// EncodeValue is the ValueEncoderFunc for a value that may be addressable. +func (cae *condAddrEncoder) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if val.CanAddr() { + return cae.canAddrEnc.EncodeValue(ec, vw, val) + } + if cae.elseEnc != nil { + return cae.elseEnc.EncodeValue(ec, vw, val) + } + return ErrNoEncoder{Type: val.Type()} +} + +// condAddrDecoder is the decoder used when a pointer to the value has a decoder. +type condAddrDecoder struct { + canAddrDec ValueDecoder + elseDec ValueDecoder +} + +var _ ValueDecoder = (*condAddrDecoder)(nil) + +// newCondAddrDecoder returns an CondAddrDecoder. +func newCondAddrDecoder(canAddrDec, elseDec ValueDecoder) *condAddrDecoder { + decoder := condAddrDecoder{canAddrDec: canAddrDec, elseDec: elseDec} + return &decoder +} + +// DecodeValue is the ValueDecoderFunc for a value that may be addressable. +func (cad *condAddrDecoder) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if val.CanAddr() { + return cad.canAddrDec.DecodeValue(dc, vr, val) + } + if cad.elseDec != nil { + return cad.elseDec.DecodeValue(dc, vr, val) + } + return ErrNoDecoder{Type: val.Type()} +} diff --git a/mongo/bson/bsoncodec/cond_addr_codec_test.go b/mongo/bson/bsoncodec/cond_addr_codec_test.go new file mode 100644 index 0000000..6bac727 --- /dev/null +++ b/mongo/bson/bsoncodec/cond_addr_codec_test.go @@ -0,0 +1,97 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "testing" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + "go.mongodb.org/mongo-driver/internal/testutil/assert" +) + +func TestCondAddrCodec(t *testing.T) { + var inner int + canAddrVal := reflect.ValueOf(&inner) + addressable := canAddrVal.Elem() + unaddressable := reflect.ValueOf(inner) + rw := &bsonrwtest.ValueReaderWriter{} + + t.Run("addressEncode", func(t *testing.T) { + invoked := 0 + encode1 := ValueEncoderFunc(func(EncodeContext, bsonrw.ValueWriter, reflect.Value) error { + invoked = 1 + return nil + }) + encode2 := ValueEncoderFunc(func(EncodeContext, bsonrw.ValueWriter, reflect.Value) error { + invoked = 2 + return nil + }) + condEncoder := newCondAddrEncoder(encode1, encode2) + + testCases := []struct { + name string + val reflect.Value + invoked int + }{ + {"canAddr", addressable, 1}, + {"else", unaddressable, 2}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := condEncoder.EncodeValue(EncodeContext{}, rw, tc.val) + assert.Nil(t, err, "CondAddrEncoder error: %v", err) + + assert.Equal(t, invoked, tc.invoked, "Expected function %v to be called, called %v", tc.invoked, invoked) + }) + } + + t.Run("error", func(t *testing.T) { + errEncoder := newCondAddrEncoder(encode1, nil) + err := errEncoder.EncodeValue(EncodeContext{}, rw, unaddressable) + want := ErrNoEncoder{Type: unaddressable.Type()} + assert.Equal(t, err, want, "expected error %v, got %v", want, err) + }) + }) + t.Run("addressDecode", func(t *testing.T) { + invoked := 0 + decode1 := ValueDecoderFunc(func(DecodeContext, bsonrw.ValueReader, reflect.Value) error { + invoked = 1 + return nil + }) + decode2 := ValueDecoderFunc(func(DecodeContext, bsonrw.ValueReader, reflect.Value) error { + invoked = 2 + return nil + }) + condDecoder := newCondAddrDecoder(decode1, decode2) + + testCases := []struct { + name string + val reflect.Value + invoked int + }{ + {"canAddr", addressable, 1}, + {"else", unaddressable, 2}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := condDecoder.DecodeValue(DecodeContext{}, rw, tc.val) + assert.Nil(t, err, "CondAddrDecoder error: %v", err) + + assert.Equal(t, invoked, tc.invoked, "Expected function %v to be called, called %v", tc.invoked, invoked) + }) + } + + t.Run("error", func(t *testing.T) { + errDecoder := newCondAddrDecoder(decode1, nil) + err := errDecoder.DecodeValue(DecodeContext{}, rw, unaddressable) + want := ErrNoDecoder{Type: unaddressable.Type()} + assert.Equal(t, err, want, "expected error %v, got %v", want, err) + }) + }) +} diff --git a/mongo/bson/bsoncodec/default_value_decoders.go b/mongo/bson/bsoncodec/default_value_decoders.go new file mode 100644 index 0000000..e95cab5 --- /dev/null +++ b/mongo/bson/bsoncodec/default_value_decoders.go @@ -0,0 +1,1729 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "net/url" + "reflect" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +var ( + defaultValueDecoders DefaultValueDecoders + errCannotTruncate = errors.New("float64 can only be truncated to an integer type when truncation is enabled") +) + +type decodeBinaryError struct { + subtype byte + typeName string +} + +func (d decodeBinaryError) Error() string { + return fmt.Sprintf("only binary values with subtype 0x00 or 0x02 can be decoded into %s, but got subtype %v", d.typeName, d.subtype) +} + +func newDefaultStructCodec() *StructCodec { + codec, err := NewStructCodec(DefaultStructTagParser) + if err != nil { + // This function is called from the codec registration path, so errors can't be propagated. If there's an error + // constructing the StructCodec, we panic to avoid losing it. + panic(fmt.Errorf("error creating default StructCodec: %v", err)) + } + return codec +} + +// DefaultValueDecoders is a namespace type for the default ValueDecoders used +// when creating a registry. +type DefaultValueDecoders struct{} + +// RegisterDefaultDecoders will register the decoder methods attached to DefaultValueDecoders with +// the provided RegistryBuilder. +// +// There is no support for decoding map[string]interface{} because there is no decoder for +// interface{}, so users must either register this decoder themselves or use the +// EmptyInterfaceDecoder available in the bson package. +func (dvd DefaultValueDecoders) RegisterDefaultDecoders(rb *RegistryBuilder) { + if rb == nil { + panic(errors.New("argument to RegisterDefaultDecoders must not be nil")) + } + + intDecoder := decodeAdapter{dvd.IntDecodeValue, dvd.intDecodeType} + floatDecoder := decodeAdapter{dvd.FloatDecodeValue, dvd.floatDecodeType} + + rb. + RegisterTypeDecoder(tD, ValueDecoderFunc(dvd.DDecodeValue)). + RegisterTypeDecoder(tBinary, decodeAdapter{dvd.BinaryDecodeValue, dvd.binaryDecodeType}). + RegisterTypeDecoder(tUndefined, decodeAdapter{dvd.UndefinedDecodeValue, dvd.undefinedDecodeType}). + RegisterTypeDecoder(tDateTime, decodeAdapter{dvd.DateTimeDecodeValue, dvd.dateTimeDecodeType}). + RegisterTypeDecoder(tNull, decodeAdapter{dvd.NullDecodeValue, dvd.nullDecodeType}). + RegisterTypeDecoder(tRegex, decodeAdapter{dvd.RegexDecodeValue, dvd.regexDecodeType}). + RegisterTypeDecoder(tDBPointer, decodeAdapter{dvd.DBPointerDecodeValue, dvd.dBPointerDecodeType}). + RegisterTypeDecoder(tTimestamp, decodeAdapter{dvd.TimestampDecodeValue, dvd.timestampDecodeType}). + RegisterTypeDecoder(tMinKey, decodeAdapter{dvd.MinKeyDecodeValue, dvd.minKeyDecodeType}). + RegisterTypeDecoder(tMaxKey, decodeAdapter{dvd.MaxKeyDecodeValue, dvd.maxKeyDecodeType}). + RegisterTypeDecoder(tJavaScript, decodeAdapter{dvd.JavaScriptDecodeValue, dvd.javaScriptDecodeType}). + RegisterTypeDecoder(tSymbol, decodeAdapter{dvd.SymbolDecodeValue, dvd.symbolDecodeType}). + RegisterTypeDecoder(tByteSlice, defaultByteSliceCodec). + RegisterTypeDecoder(tTime, defaultTimeCodec). + RegisterTypeDecoder(tEmpty, defaultEmptyInterfaceCodec). + RegisterTypeDecoder(tCoreArray, defaultArrayCodec). + RegisterTypeDecoder(tOID, decodeAdapter{dvd.ObjectIDDecodeValue, dvd.objectIDDecodeType}). + RegisterTypeDecoder(tDecimal, decodeAdapter{dvd.Decimal128DecodeValue, dvd.decimal128DecodeType}). + RegisterTypeDecoder(tJSONNumber, decodeAdapter{dvd.JSONNumberDecodeValue, dvd.jsonNumberDecodeType}). + RegisterTypeDecoder(tURL, decodeAdapter{dvd.URLDecodeValue, dvd.urlDecodeType}). + RegisterTypeDecoder(tCoreDocument, ValueDecoderFunc(dvd.CoreDocumentDecodeValue)). + RegisterTypeDecoder(tCodeWithScope, decodeAdapter{dvd.CodeWithScopeDecodeValue, dvd.codeWithScopeDecodeType}). + RegisterDefaultDecoder(reflect.Bool, decodeAdapter{dvd.BooleanDecodeValue, dvd.booleanDecodeType}). + RegisterDefaultDecoder(reflect.Int, intDecoder). + RegisterDefaultDecoder(reflect.Int8, intDecoder). + RegisterDefaultDecoder(reflect.Int16, intDecoder). + RegisterDefaultDecoder(reflect.Int32, intDecoder). + RegisterDefaultDecoder(reflect.Int64, intDecoder). + RegisterDefaultDecoder(reflect.Uint, defaultUIntCodec). + RegisterDefaultDecoder(reflect.Uint8, defaultUIntCodec). + RegisterDefaultDecoder(reflect.Uint16, defaultUIntCodec). + RegisterDefaultDecoder(reflect.Uint32, defaultUIntCodec). + RegisterDefaultDecoder(reflect.Uint64, defaultUIntCodec). + RegisterDefaultDecoder(reflect.Float32, floatDecoder). + RegisterDefaultDecoder(reflect.Float64, floatDecoder). + RegisterDefaultDecoder(reflect.Array, ValueDecoderFunc(dvd.ArrayDecodeValue)). + RegisterDefaultDecoder(reflect.Map, defaultMapCodec). + RegisterDefaultDecoder(reflect.Slice, defaultSliceCodec). + RegisterDefaultDecoder(reflect.String, defaultStringCodec). + RegisterDefaultDecoder(reflect.Struct, newDefaultStructCodec()). + RegisterDefaultDecoder(reflect.Ptr, NewPointerCodec()). + RegisterTypeMapEntry(bsontype.Double, tFloat64). + RegisterTypeMapEntry(bsontype.String, tString). + RegisterTypeMapEntry(bsontype.Array, tA). + RegisterTypeMapEntry(bsontype.Binary, tBinary). + RegisterTypeMapEntry(bsontype.Undefined, tUndefined). + RegisterTypeMapEntry(bsontype.ObjectID, tOID). + RegisterTypeMapEntry(bsontype.Boolean, tBool). + RegisterTypeMapEntry(bsontype.DateTime, tDateTime). + RegisterTypeMapEntry(bsontype.Regex, tRegex). + RegisterTypeMapEntry(bsontype.DBPointer, tDBPointer). + RegisterTypeMapEntry(bsontype.JavaScript, tJavaScript). + RegisterTypeMapEntry(bsontype.Symbol, tSymbol). + RegisterTypeMapEntry(bsontype.CodeWithScope, tCodeWithScope). + RegisterTypeMapEntry(bsontype.Int32, tInt32). + RegisterTypeMapEntry(bsontype.Int64, tInt64). + RegisterTypeMapEntry(bsontype.Timestamp, tTimestamp). + RegisterTypeMapEntry(bsontype.Decimal128, tDecimal). + RegisterTypeMapEntry(bsontype.MinKey, tMinKey). + RegisterTypeMapEntry(bsontype.MaxKey, tMaxKey). + RegisterTypeMapEntry(bsontype.Type(0), tD). + RegisterTypeMapEntry(bsontype.EmbeddedDocument, tD). + RegisterHookDecoder(tValueUnmarshaler, ValueDecoderFunc(dvd.ValueUnmarshalerDecodeValue)). + RegisterHookDecoder(tUnmarshaler, ValueDecoderFunc(dvd.UnmarshalerDecodeValue)) +} + +// DDecodeValue is the ValueDecoderFunc for primitive.D instances. +func (dvd DefaultValueDecoders) DDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || !val.CanSet() || val.Type() != tD { + return ValueDecoderError{Name: "DDecodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: val} + } + + switch vrType := vr.Type(); vrType { + case bsontype.Type(0), bsontype.EmbeddedDocument: + dc.Ancestor = tD + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + default: + return fmt.Errorf("cannot decode %v into a primitive.D", vrType) + } + + dr, err := vr.ReadDocument() + if err != nil { + return err + } + + decoder, err := dc.LookupDecoder(tEmpty) + if err != nil { + return err + } + tEmptyTypeDecoder, _ := decoder.(typeDecoder) + + // Use the elements in the provided value if it's non nil. Otherwise, allocate a new D instance. + var elems primitive.D + if !val.IsNil() { + val.SetLen(0) + elems = val.Interface().(primitive.D) + } else { + elems = make(primitive.D, 0) + } + + for { + key, elemVr, err := dr.ReadElement() + if err == bsonrw.ErrEOD { + break + } else if err != nil { + return err + } + + // Pass false for convert because we don't need to call reflect.Value.Convert for tEmpty. + elem, err := decodeTypeOrValueWithInfo(decoder, tEmptyTypeDecoder, dc, elemVr, tEmpty, false) + if err != nil { + return err + } + + elems = append(elems, primitive.E{Key: key, Value: elem.Interface()}) + } + + val.Set(reflect.ValueOf(elems)) + return nil +} + +func (dvd DefaultValueDecoders) booleanDecodeType(dctx DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t.Kind() != reflect.Bool { + return emptyValue, ValueDecoderError{ + Name: "BooleanDecodeValue", + Kinds: []reflect.Kind{reflect.Bool}, + Received: reflect.Zero(t), + } + } + + var b bool + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return emptyValue, err + } + b = (i32 != 0) + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return emptyValue, err + } + b = (i64 != 0) + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return emptyValue, err + } + b = (f64 != 0) + case bsontype.Boolean: + b, err = vr.ReadBoolean() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a boolean", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(b), nil +} + +// BooleanDecodeValue is the ValueDecoderFunc for bool types. +func (dvd DefaultValueDecoders) BooleanDecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || !val.CanSet() || val.Kind() != reflect.Bool { + return ValueDecoderError{Name: "BooleanDecodeValue", Kinds: []reflect.Kind{reflect.Bool}, Received: val} + } + + elem, err := dvd.booleanDecodeType(dctx, vr, val.Type()) + if err != nil { + return err + } + + val.SetBool(elem.Bool()) + return nil +} + +func (DefaultValueDecoders) intDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + var i64 int64 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return emptyValue, err + } + i64 = int64(i32) + case bsontype.Int64: + i64, err = vr.ReadInt64() + if err != nil { + return emptyValue, err + } + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return emptyValue, err + } + if !dc.Truncate && math.Floor(f64) != f64 { + return emptyValue, errCannotTruncate + } + if f64 > float64(math.MaxInt64) { + return emptyValue, fmt.Errorf("%g overflows int64", f64) + } + i64 = int64(f64) + case bsontype.Boolean: + b, err := vr.ReadBoolean() + if err != nil { + return emptyValue, err + } + if b { + i64 = 1 + } + case bsontype.Null: + if err = vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err = vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into an integer type", vrType) + } + + switch t.Kind() { + case reflect.Int8: + if i64 < math.MinInt8 || i64 > math.MaxInt8 { + return emptyValue, fmt.Errorf("%d overflows int8", i64) + } + + return reflect.ValueOf(int8(i64)), nil + case reflect.Int16: + if i64 < math.MinInt16 || i64 > math.MaxInt16 { + return emptyValue, fmt.Errorf("%d overflows int16", i64) + } + + return reflect.ValueOf(int16(i64)), nil + case reflect.Int32: + if i64 < math.MinInt32 || i64 > math.MaxInt32 { + return emptyValue, fmt.Errorf("%d overflows int32", i64) + } + + return reflect.ValueOf(int32(i64)), nil + case reflect.Int64: + return reflect.ValueOf(i64), nil + case reflect.Int: + if int64(int(i64)) != i64 { // Can we fit this inside of an int + return emptyValue, fmt.Errorf("%d overflows int", i64) + } + + return reflect.ValueOf(int(i64)), nil + default: + return emptyValue, ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.Zero(t), + } + } +} + +// IntDecodeValue is the ValueDecoderFunc for int types. +func (dvd DefaultValueDecoders) IntDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() { + return ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: val, + } + } + + elem, err := dvd.intDecodeType(dc, vr, val.Type()) + if err != nil { + return err + } + + val.SetInt(elem.Int()) + return nil +} + +// UintDecodeValue is the ValueDecoderFunc for uint types. +// +// Deprecated: UintDecodeValue is not registered by default. Use UintCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) UintDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + var i64 int64 + var err error + switch vr.Type() { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return err + } + i64 = int64(i32) + case bsontype.Int64: + i64, err = vr.ReadInt64() + if err != nil { + return err + } + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return err + } + if !dc.Truncate && math.Floor(f64) != f64 { + return errors.New("UintDecodeValue can only truncate float64 to an integer type when truncation is enabled") + } + if f64 > float64(math.MaxInt64) { + return fmt.Errorf("%g overflows int64", f64) + } + i64 = int64(f64) + case bsontype.Boolean: + b, err := vr.ReadBoolean() + if err != nil { + return err + } + if b { + i64 = 1 + } + default: + return fmt.Errorf("cannot decode %v into an integer type", vr.Type()) + } + + if !val.CanSet() { + return ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: val, + } + } + + switch val.Kind() { + case reflect.Uint8: + if i64 < 0 || i64 > math.MaxUint8 { + return fmt.Errorf("%d overflows uint8", i64) + } + case reflect.Uint16: + if i64 < 0 || i64 > math.MaxUint16 { + return fmt.Errorf("%d overflows uint16", i64) + } + case reflect.Uint32: + if i64 < 0 || i64 > math.MaxUint32 { + return fmt.Errorf("%d overflows uint32", i64) + } + case reflect.Uint64: + if i64 < 0 { + return fmt.Errorf("%d overflows uint64", i64) + } + case reflect.Uint: + if i64 < 0 || int64(uint(i64)) != i64 { // Can we fit this inside of an uint + return fmt.Errorf("%d overflows uint", i64) + } + default: + return ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: val, + } + } + + val.SetUint(uint64(i64)) + return nil +} + +func (dvd DefaultValueDecoders) floatDecodeType(ec DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + var f float64 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return emptyValue, err + } + f = float64(i32) + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return emptyValue, err + } + f = float64(i64) + case bsontype.Double: + f, err = vr.ReadDouble() + if err != nil { + return emptyValue, err + } + case bsontype.Boolean: + b, err := vr.ReadBoolean() + if err != nil { + return emptyValue, err + } + if b { + f = 1 + } + case bsontype.Null: + if err = vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err = vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into a float32 or float64 type", vrType) + } + + switch t.Kind() { + case reflect.Float32: + if !ec.Truncate && float64(float32(f)) != f { + return emptyValue, errCannotTruncate + } + + return reflect.ValueOf(float32(f)), nil + case reflect.Float64: + return reflect.ValueOf(f), nil + default: + return emptyValue, ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: reflect.Zero(t), + } + } +} + +// FloatDecodeValue is the ValueDecoderFunc for float types. +func (dvd DefaultValueDecoders) FloatDecodeValue(ec DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() { + return ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: val, + } + } + + elem, err := dvd.floatDecodeType(ec, vr, val.Type()) + if err != nil { + return err + } + + val.SetFloat(elem.Float()) + return nil +} + +// StringDecodeValue is the ValueDecoderFunc for string types. +// +// Deprecated: StringDecodeValue is not registered by default. Use StringCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) StringDecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + var str string + var err error + switch vr.Type() { + // TODO(GODRIVER-577): Handle JavaScript and Symbol BSON types when allowed. + case bsontype.String: + str, err = vr.ReadString() + if err != nil { + return err + } + default: + return fmt.Errorf("cannot decode %v into a string type", vr.Type()) + } + if !val.CanSet() || val.Kind() != reflect.String { + return ValueDecoderError{Name: "StringDecodeValue", Kinds: []reflect.Kind{reflect.String}, Received: val} + } + + val.SetString(str) + return nil +} + +func (DefaultValueDecoders) javaScriptDecodeType(dctx DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tJavaScript { + return emptyValue, ValueDecoderError{ + Name: "JavaScriptDecodeValue", + Types: []reflect.Type{tJavaScript}, + Received: reflect.Zero(t), + } + } + + var js string + var err error + switch vrType := vr.Type(); vrType { + case bsontype.JavaScript: + js, err = vr.ReadJavascript() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a primitive.JavaScript", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.JavaScript(js)), nil +} + +// JavaScriptDecodeValue is the ValueDecoderFunc for the primitive.JavaScript type. +func (dvd DefaultValueDecoders) JavaScriptDecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tJavaScript { + return ValueDecoderError{Name: "JavaScriptDecodeValue", Types: []reflect.Type{tJavaScript}, Received: val} + } + + elem, err := dvd.javaScriptDecodeType(dctx, vr, tJavaScript) + if err != nil { + return err + } + + val.SetString(elem.String()) + return nil +} + +func (DefaultValueDecoders) symbolDecodeType(dctx DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tSymbol { + return emptyValue, ValueDecoderError{ + Name: "SymbolDecodeValue", + Types: []reflect.Type{tSymbol}, + Received: reflect.Zero(t), + } + } + + var symbol string + var err error + switch vrType := vr.Type(); vrType { + case bsontype.String: + symbol, err = vr.ReadString() + case bsontype.Symbol: + symbol, err = vr.ReadSymbol() + case bsontype.Binary: + data, subtype, err := vr.ReadBinary() + if err != nil { + return emptyValue, err + } + + if subtype != bsontype.BinaryGeneric && subtype != bsontype.BinaryBinaryOld { + return emptyValue, decodeBinaryError{subtype: subtype, typeName: "primitive.Symbol"} + } + symbol = string(data) + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a primitive.Symbol", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Symbol(symbol)), nil +} + +// SymbolDecodeValue is the ValueDecoderFunc for the primitive.Symbol type. +func (dvd DefaultValueDecoders) SymbolDecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tSymbol { + return ValueDecoderError{Name: "SymbolDecodeValue", Types: []reflect.Type{tSymbol}, Received: val} + } + + elem, err := dvd.symbolDecodeType(dctx, vr, tSymbol) + if err != nil { + return err + } + + val.SetString(elem.String()) + return nil +} + +func (DefaultValueDecoders) binaryDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tBinary { + return emptyValue, ValueDecoderError{ + Name: "BinaryDecodeValue", + Types: []reflect.Type{tBinary}, + Received: reflect.Zero(t), + } + } + + var data []byte + var subtype byte + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Binary: + data, subtype, err = vr.ReadBinary() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a Binary", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Binary{Subtype: subtype, Data: data}), nil +} + +// BinaryDecodeValue is the ValueDecoderFunc for Binary. +func (dvd DefaultValueDecoders) BinaryDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tBinary { + return ValueDecoderError{Name: "BinaryDecodeValue", Types: []reflect.Type{tBinary}, Received: val} + } + + elem, err := dvd.binaryDecodeType(dc, vr, tBinary) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) undefinedDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tUndefined { + return emptyValue, ValueDecoderError{ + Name: "UndefinedDecodeValue", + Types: []reflect.Type{tUndefined}, + Received: reflect.Zero(t), + } + } + + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Undefined: + err = vr.ReadUndefined() + case bsontype.Null: + err = vr.ReadNull() + default: + return emptyValue, fmt.Errorf("cannot decode %v into an Undefined", vr.Type()) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Undefined{}), nil +} + +// UndefinedDecodeValue is the ValueDecoderFunc for Undefined. +func (dvd DefaultValueDecoders) UndefinedDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tUndefined { + return ValueDecoderError{Name: "UndefinedDecodeValue", Types: []reflect.Type{tUndefined}, Received: val} + } + + elem, err := dvd.undefinedDecodeType(dc, vr, tUndefined) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +// Accept both 12-byte string and pretty-printed 24-byte hex string formats. +func (dvd DefaultValueDecoders) objectIDDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tOID { + return emptyValue, ValueDecoderError{ + Name: "ObjectIDDecodeValue", + Types: []reflect.Type{tOID}, + Received: reflect.Zero(t), + } + } + + var oid primitive.ObjectID + var err error + switch vrType := vr.Type(); vrType { + case bsontype.ObjectID: + oid, err = vr.ReadObjectID() + if err != nil { + return emptyValue, err + } + case bsontype.String: + str, err := vr.ReadString() + if err != nil { + return emptyValue, err + } + if oid, err = primitive.ObjectIDFromHex(str); err == nil { + break + } + if len(str) != 12 { + return emptyValue, fmt.Errorf("an ObjectID string must be exactly 12 bytes long (got %v)", len(str)) + } + byteArr := []byte(str) + copy(oid[:], byteArr) + case bsontype.Null: + if err = vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err = vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into an ObjectID", vrType) + } + + return reflect.ValueOf(oid), nil +} + +// ObjectIDDecodeValue is the ValueDecoderFunc for primitive.ObjectID. +func (dvd DefaultValueDecoders) ObjectIDDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tOID { + return ValueDecoderError{Name: "ObjectIDDecodeValue", Types: []reflect.Type{tOID}, Received: val} + } + + elem, err := dvd.objectIDDecodeType(dc, vr, tOID) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) dateTimeDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tDateTime { + return emptyValue, ValueDecoderError{ + Name: "DateTimeDecodeValue", + Types: []reflect.Type{tDateTime}, + Received: reflect.Zero(t), + } + } + + var dt int64 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.DateTime: + dt, err = vr.ReadDateTime() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a DateTime", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.DateTime(dt)), nil +} + +// DateTimeDecodeValue is the ValueDecoderFunc for DateTime. +func (dvd DefaultValueDecoders) DateTimeDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tDateTime { + return ValueDecoderError{Name: "DateTimeDecodeValue", Types: []reflect.Type{tDateTime}, Received: val} + } + + elem, err := dvd.dateTimeDecodeType(dc, vr, tDateTime) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) nullDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tNull { + return emptyValue, ValueDecoderError{ + Name: "NullDecodeValue", + Types: []reflect.Type{tNull}, + Received: reflect.Zero(t), + } + } + + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Undefined: + err = vr.ReadUndefined() + case bsontype.Null: + err = vr.ReadNull() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a Null", vr.Type()) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Null{}), nil +} + +// NullDecodeValue is the ValueDecoderFunc for Null. +func (dvd DefaultValueDecoders) NullDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tNull { + return ValueDecoderError{Name: "NullDecodeValue", Types: []reflect.Type{tNull}, Received: val} + } + + elem, err := dvd.nullDecodeType(dc, vr, tNull) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) regexDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tRegex { + return emptyValue, ValueDecoderError{ + Name: "RegexDecodeValue", + Types: []reflect.Type{tRegex}, + Received: reflect.Zero(t), + } + } + + var pattern, options string + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Regex: + pattern, options, err = vr.ReadRegex() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a Regex", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Regex{Pattern: pattern, Options: options}), nil +} + +// RegexDecodeValue is the ValueDecoderFunc for Regex. +func (dvd DefaultValueDecoders) RegexDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tRegex { + return ValueDecoderError{Name: "RegexDecodeValue", Types: []reflect.Type{tRegex}, Received: val} + } + + elem, err := dvd.regexDecodeType(dc, vr, tRegex) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) dBPointerDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tDBPointer { + return emptyValue, ValueDecoderError{ + Name: "DBPointerDecodeValue", + Types: []reflect.Type{tDBPointer}, + Received: reflect.Zero(t), + } + } + + var ns string + var pointer primitive.ObjectID + var err error + switch vrType := vr.Type(); vrType { + case bsontype.DBPointer: + ns, pointer, err = vr.ReadDBPointer() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a DBPointer", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.DBPointer{DB: ns, Pointer: pointer}), nil +} + +// DBPointerDecodeValue is the ValueDecoderFunc for DBPointer. +func (dvd DefaultValueDecoders) DBPointerDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tDBPointer { + return ValueDecoderError{Name: "DBPointerDecodeValue", Types: []reflect.Type{tDBPointer}, Received: val} + } + + elem, err := dvd.dBPointerDecodeType(dc, vr, tDBPointer) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) timestampDecodeType(dc DecodeContext, vr bsonrw.ValueReader, reflectType reflect.Type) (reflect.Value, error) { + if reflectType != tTimestamp { + return emptyValue, ValueDecoderError{ + Name: "TimestampDecodeValue", + Types: []reflect.Type{tTimestamp}, + Received: reflect.Zero(reflectType), + } + } + + var t, incr uint32 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Timestamp: + t, incr, err = vr.ReadTimestamp() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a Timestamp", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.Timestamp{T: t, I: incr}), nil +} + +// TimestampDecodeValue is the ValueDecoderFunc for Timestamp. +func (dvd DefaultValueDecoders) TimestampDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tTimestamp { + return ValueDecoderError{Name: "TimestampDecodeValue", Types: []reflect.Type{tTimestamp}, Received: val} + } + + elem, err := dvd.timestampDecodeType(dc, vr, tTimestamp) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) minKeyDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tMinKey { + return emptyValue, ValueDecoderError{ + Name: "MinKeyDecodeValue", + Types: []reflect.Type{tMinKey}, + Received: reflect.Zero(t), + } + } + + var err error + switch vrType := vr.Type(); vrType { + case bsontype.MinKey: + err = vr.ReadMinKey() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a MinKey", vr.Type()) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.MinKey{}), nil +} + +// MinKeyDecodeValue is the ValueDecoderFunc for MinKey. +func (dvd DefaultValueDecoders) MinKeyDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tMinKey { + return ValueDecoderError{Name: "MinKeyDecodeValue", Types: []reflect.Type{tMinKey}, Received: val} + } + + elem, err := dvd.minKeyDecodeType(dc, vr, tMinKey) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (DefaultValueDecoders) maxKeyDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tMaxKey { + return emptyValue, ValueDecoderError{ + Name: "MaxKeyDecodeValue", + Types: []reflect.Type{tMaxKey}, + Received: reflect.Zero(t), + } + } + + var err error + switch vrType := vr.Type(); vrType { + case bsontype.MaxKey: + err = vr.ReadMaxKey() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a MaxKey", vr.Type()) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(primitive.MaxKey{}), nil +} + +// MaxKeyDecodeValue is the ValueDecoderFunc for MaxKey. +func (dvd DefaultValueDecoders) MaxKeyDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tMaxKey { + return ValueDecoderError{Name: "MaxKeyDecodeValue", Types: []reflect.Type{tMaxKey}, Received: val} + } + + elem, err := dvd.maxKeyDecodeType(dc, vr, tMaxKey) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (dvd DefaultValueDecoders) decimal128DecodeType(dctx DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tDecimal { + return emptyValue, ValueDecoderError{ + Name: "Decimal128DecodeValue", + Types: []reflect.Type{tDecimal}, + Received: reflect.Zero(t), + } + } + + var d128 primitive.Decimal128 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Decimal128: + d128, err = vr.ReadDecimal128() + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a primitive.Decimal128", vr.Type()) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(d128), nil +} + +// Decimal128DecodeValue is the ValueDecoderFunc for primitive.Decimal128. +func (dvd DefaultValueDecoders) Decimal128DecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tDecimal { + return ValueDecoderError{Name: "Decimal128DecodeValue", Types: []reflect.Type{tDecimal}, Received: val} + } + + elem, err := dvd.decimal128DecodeType(dctx, vr, tDecimal) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (dvd DefaultValueDecoders) jsonNumberDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tJSONNumber { + return emptyValue, ValueDecoderError{ + Name: "JSONNumberDecodeValue", + Types: []reflect.Type{tJSONNumber}, + Received: reflect.Zero(t), + } + } + + var jsonNum json.Number + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return emptyValue, err + } + jsonNum = json.Number(strconv.FormatFloat(f64, 'f', -1, 64)) + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return emptyValue, err + } + jsonNum = json.Number(strconv.FormatInt(int64(i32), 10)) + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return emptyValue, err + } + jsonNum = json.Number(strconv.FormatInt(i64, 10)) + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a json.Number", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(jsonNum), nil +} + +// JSONNumberDecodeValue is the ValueDecoderFunc for json.Number. +func (dvd DefaultValueDecoders) JSONNumberDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tJSONNumber { + return ValueDecoderError{Name: "JSONNumberDecodeValue", Types: []reflect.Type{tJSONNumber}, Received: val} + } + + elem, err := dvd.jsonNumberDecodeType(dc, vr, tJSONNumber) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (dvd DefaultValueDecoders) urlDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tURL { + return emptyValue, ValueDecoderError{ + Name: "URLDecodeValue", + Types: []reflect.Type{tURL}, + Received: reflect.Zero(t), + } + } + + urlPtr := &url.URL{} + var err error + switch vrType := vr.Type(); vrType { + case bsontype.String: + var str string // Declare str here to avoid shadowing err during the ReadString call. + str, err = vr.ReadString() + if err != nil { + return emptyValue, err + } + + urlPtr, err = url.Parse(str) + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a *url.URL", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(urlPtr).Elem(), nil +} + +// URLDecodeValue is the ValueDecoderFunc for url.URL. +func (dvd DefaultValueDecoders) URLDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tURL { + return ValueDecoderError{Name: "URLDecodeValue", Types: []reflect.Type{tURL}, Received: val} + } + + elem, err := dvd.urlDecodeType(dc, vr, tURL) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +// TimeDecodeValue is the ValueDecoderFunc for time.Time. +// +// Deprecated: TimeDecodeValue is not registered by default. Use TimeCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) TimeDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if vr.Type() != bsontype.DateTime { + return fmt.Errorf("cannot decode %v into a time.Time", vr.Type()) + } + + dt, err := vr.ReadDateTime() + if err != nil { + return err + } + + if !val.CanSet() || val.Type() != tTime { + return ValueDecoderError{Name: "TimeDecodeValue", Types: []reflect.Type{tTime}, Received: val} + } + + val.Set(reflect.ValueOf(time.Unix(dt/1000, dt%1000*1000000).UTC())) + return nil +} + +// ByteSliceDecodeValue is the ValueDecoderFunc for []byte. +// +// Deprecated: ByteSliceDecodeValue is not registered by default. Use ByteSliceCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) ByteSliceDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if vr.Type() != bsontype.Binary && vr.Type() != bsontype.Null { + return fmt.Errorf("cannot decode %v into a []byte", vr.Type()) + } + + if !val.CanSet() || val.Type() != tByteSlice { + return ValueDecoderError{Name: "ByteSliceDecodeValue", Types: []reflect.Type{tByteSlice}, Received: val} + } + + if vr.Type() == bsontype.Null { + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + } + + data, subtype, err := vr.ReadBinary() + if err != nil { + return err + } + if subtype != 0x00 { + return fmt.Errorf("ByteSliceDecodeValue can only be used to decode subtype 0x00 for %s, got %v", bsontype.Binary, subtype) + } + + val.Set(reflect.ValueOf(data)) + return nil +} + +// MapDecodeValue is the ValueDecoderFunc for map[string]* types. +// +// Deprecated: MapDecodeValue is not registered by default. Use MapCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) MapDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.Map || val.Type().Key().Kind() != reflect.String { + return ValueDecoderError{Name: "MapDecodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: val} + } + + switch vr.Type() { + case bsontype.Type(0), bsontype.EmbeddedDocument: + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + default: + return fmt.Errorf("cannot decode %v into a %s", vr.Type(), val.Type()) + } + + dr, err := vr.ReadDocument() + if err != nil { + return err + } + + if val.IsNil() { + val.Set(reflect.MakeMap(val.Type())) + } + + eType := val.Type().Elem() + decoder, err := dc.LookupDecoder(eType) + if err != nil { + return err + } + + if eType == tEmpty { + dc.Ancestor = val.Type() + } + + keyType := val.Type().Key() + for { + key, vr, err := dr.ReadElement() + if err == bsonrw.ErrEOD { + break + } + if err != nil { + return err + } + + elem := reflect.New(eType).Elem() + + err = decoder.DecodeValue(dc, vr, elem) + if err != nil { + return err + } + + val.SetMapIndex(reflect.ValueOf(key).Convert(keyType), elem) + } + return nil +} + +// ArrayDecodeValue is the ValueDecoderFunc for array types. +func (dvd DefaultValueDecoders) ArrayDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Array { + return ValueDecoderError{Name: "ArrayDecodeValue", Kinds: []reflect.Kind{reflect.Array}, Received: val} + } + + switch vrType := vr.Type(); vrType { + case bsontype.Array: + case bsontype.Type(0), bsontype.EmbeddedDocument: + if val.Type().Elem() != tE { + return fmt.Errorf("cannot decode document into %s", val.Type()) + } + case bsontype.Binary: + if val.Type().Elem() != tByte { + return fmt.Errorf("ArrayDecodeValue can only be used to decode binary into a byte array, got %v", vrType) + } + data, subtype, err := vr.ReadBinary() + if err != nil { + return err + } + if subtype != bsontype.BinaryGeneric && subtype != bsontype.BinaryBinaryOld { + return fmt.Errorf("ArrayDecodeValue can only be used to decode subtype 0x00 or 0x02 for %s, got %v", bsontype.Binary, subtype) + } + + if len(data) > val.Len() { + return fmt.Errorf("more elements returned in array than can fit inside %s", val.Type()) + } + + for idx, elem := range data { + val.Index(idx).Set(reflect.ValueOf(elem)) + } + return nil + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + case bsontype.Undefined: + val.Set(reflect.Zero(val.Type())) + return vr.ReadUndefined() + default: + return fmt.Errorf("cannot decode %v into an array", vrType) + } + + var elemsFunc func(DecodeContext, bsonrw.ValueReader, reflect.Value) ([]reflect.Value, error) + switch val.Type().Elem() { + case tE: + elemsFunc = dvd.decodeD + default: + elemsFunc = dvd.decodeDefault + } + + elems, err := elemsFunc(dc, vr, val) + if err != nil { + return err + } + + if len(elems) > val.Len() { + return fmt.Errorf("more elements returned in array than can fit inside %s, got %v elements", val.Type(), len(elems)) + } + + for idx, elem := range elems { + val.Index(idx).Set(elem) + } + + return nil +} + +// SliceDecodeValue is the ValueDecoderFunc for slice types. +// +// Deprecated: SliceDecodeValue is not registered by default. Use SliceCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) SliceDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.Slice { + return ValueDecoderError{Name: "SliceDecodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: val} + } + + switch vr.Type() { + case bsontype.Array: + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + case bsontype.Type(0), bsontype.EmbeddedDocument: + if val.Type().Elem() != tE { + return fmt.Errorf("cannot decode document into %s", val.Type()) + } + default: + return fmt.Errorf("cannot decode %v into a slice", vr.Type()) + } + + var elemsFunc func(DecodeContext, bsonrw.ValueReader, reflect.Value) ([]reflect.Value, error) + switch val.Type().Elem() { + case tE: + dc.Ancestor = val.Type() + elemsFunc = dvd.decodeD + default: + elemsFunc = dvd.decodeDefault + } + + elems, err := elemsFunc(dc, vr, val) + if err != nil { + return err + } + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, len(elems))) + } + + val.SetLen(0) + val.Set(reflect.Append(val, elems...)) + + return nil +} + +// ValueUnmarshalerDecodeValue is the ValueDecoderFunc for ValueUnmarshaler implementations. +func (dvd DefaultValueDecoders) ValueUnmarshalerDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || (!val.Type().Implements(tValueUnmarshaler) && !reflect.PtrTo(val.Type()).Implements(tValueUnmarshaler)) { + return ValueDecoderError{Name: "ValueUnmarshalerDecodeValue", Types: []reflect.Type{tValueUnmarshaler}, Received: val} + } + + if val.Kind() == reflect.Ptr && val.IsNil() { + if !val.CanSet() { + return ValueDecoderError{Name: "ValueUnmarshalerDecodeValue", Types: []reflect.Type{tValueUnmarshaler}, Received: val} + } + val.Set(reflect.New(val.Type().Elem())) + } + + if !val.Type().Implements(tValueUnmarshaler) { + if !val.CanAddr() { + return ValueDecoderError{Name: "ValueUnmarshalerDecodeValue", Types: []reflect.Type{tValueUnmarshaler}, Received: val} + } + val = val.Addr() // If the type doesn't implement the interface, a pointer to it must. + } + + t, src, err := bsonrw.Copier{}.CopyValueToBytes(vr) + if err != nil { + return err + } + + fn := val.Convert(tValueUnmarshaler).MethodByName("UnmarshalBSONValue") + errVal := fn.Call([]reflect.Value{reflect.ValueOf(t), reflect.ValueOf(src)})[0] + if !errVal.IsNil() { + return errVal.Interface().(error) + } + return nil +} + +// UnmarshalerDecodeValue is the ValueDecoderFunc for Unmarshaler implementations. +func (dvd DefaultValueDecoders) UnmarshalerDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || (!val.Type().Implements(tUnmarshaler) && !reflect.PtrTo(val.Type()).Implements(tUnmarshaler)) { + return ValueDecoderError{Name: "UnmarshalerDecodeValue", Types: []reflect.Type{tUnmarshaler}, Received: val} + } + + if val.Kind() == reflect.Ptr && val.IsNil() { + if !val.CanSet() { + return ValueDecoderError{Name: "UnmarshalerDecodeValue", Types: []reflect.Type{tUnmarshaler}, Received: val} + } + val.Set(reflect.New(val.Type().Elem())) + } + + _, src, err := bsonrw.Copier{}.CopyValueToBytes(vr) + if err != nil { + return err + } + + // If the target Go value is a pointer and the BSON field value is empty, set the value to the + // zero value of the pointer (nil) and don't call UnmarshalBSON. UnmarshalBSON has no way to + // change the pointer value from within the function (only the value at the pointer address), + // so it can't set the pointer to "nil" itself. Since the most common Go value for an empty BSON + // field value is "nil", we set "nil" here and don't call UnmarshalBSON. This behavior matches + // the behavior of the Go "encoding/json" unmarshaler when the target Go value is a pointer and + // the JSON field value is "null". + if val.Kind() == reflect.Ptr && len(src) == 0 { + val.Set(reflect.Zero(val.Type())) + return nil + } + + if !val.Type().Implements(tUnmarshaler) { + if !val.CanAddr() { + return ValueDecoderError{Name: "UnmarshalerDecodeValue", Types: []reflect.Type{tUnmarshaler}, Received: val} + } + val = val.Addr() // If the type doesn't implement the interface, a pointer to it must. + } + + fn := val.Convert(tUnmarshaler).MethodByName("UnmarshalBSON") + errVal := fn.Call([]reflect.Value{reflect.ValueOf(src)})[0] + if !errVal.IsNil() { + return errVal.Interface().(error) + } + return nil +} + +// EmptyInterfaceDecodeValue is the ValueDecoderFunc for interface{}. +// +// Deprecated: EmptyInterfaceDecodeValue is not registered by default. Use EmptyInterfaceCodec.DecodeValue instead. +func (dvd DefaultValueDecoders) EmptyInterfaceDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tEmpty { + return ValueDecoderError{Name: "EmptyInterfaceDecodeValue", Types: []reflect.Type{tEmpty}, Received: val} + } + + rtype, err := dc.LookupTypeMapEntry(vr.Type()) + if err != nil { + switch vr.Type() { + case bsontype.EmbeddedDocument: + if dc.Ancestor != nil { + rtype = dc.Ancestor + break + } + rtype = tD + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + default: + return err + } + } + + decoder, err := dc.LookupDecoder(rtype) + if err != nil { + return err + } + + elem := reflect.New(rtype).Elem() + err = decoder.DecodeValue(dc, vr, elem) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +// CoreDocumentDecodeValue is the ValueDecoderFunc for bsoncore.Document. +func (DefaultValueDecoders) CoreDocumentDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tCoreDocument { + return ValueDecoderError{Name: "CoreDocumentDecodeValue", Types: []reflect.Type{tCoreDocument}, Received: val} + } + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, 0)) + } + + val.SetLen(0) + + cdoc, err := bsonrw.Copier{}.AppendDocumentBytes(val.Interface().(bsoncore.Document), vr) + val.Set(reflect.ValueOf(cdoc)) + return err +} + +func (dvd DefaultValueDecoders) decodeDefault(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) ([]reflect.Value, error) { + elems := make([]reflect.Value, 0) + + ar, err := vr.ReadArray() + if err != nil { + return nil, err + } + + eType := val.Type().Elem() + + decoder, err := dc.LookupDecoder(eType) + if err != nil { + return nil, err + } + eTypeDecoder, _ := decoder.(typeDecoder) + + idx := 0 + for { + vr, err := ar.ReadValue() + if err == bsonrw.ErrEOA { + break + } + if err != nil { + return nil, err + } + + elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, eType, true) + if err != nil { + return nil, newDecodeError(strconv.Itoa(idx), err) + } + elems = append(elems, elem) + idx++ + } + + return elems, nil +} + +func (dvd DefaultValueDecoders) readCodeWithScope(dc DecodeContext, vr bsonrw.ValueReader) (primitive.CodeWithScope, error) { + var cws primitive.CodeWithScope + + code, dr, err := vr.ReadCodeWithScope() + if err != nil { + return cws, err + } + + scope := reflect.New(tD).Elem() + elems, err := dvd.decodeElemsFromDocumentReader(dc, dr) + if err != nil { + return cws, err + } + + scope.Set(reflect.MakeSlice(tD, 0, len(elems))) + scope.Set(reflect.Append(scope, elems...)) + + cws = primitive.CodeWithScope{ + Code: primitive.JavaScript(code), + Scope: scope.Interface().(primitive.D), + } + return cws, nil +} + +func (dvd DefaultValueDecoders) codeWithScopeDecodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tCodeWithScope { + return emptyValue, ValueDecoderError{ + Name: "CodeWithScopeDecodeValue", + Types: []reflect.Type{tCodeWithScope}, + Received: reflect.Zero(t), + } + } + + var cws primitive.CodeWithScope + var err error + switch vrType := vr.Type(); vrType { + case bsontype.CodeWithScope: + cws, err = dvd.readCodeWithScope(dc, vr) + case bsontype.Null: + err = vr.ReadNull() + case bsontype.Undefined: + err = vr.ReadUndefined() + default: + return emptyValue, fmt.Errorf("cannot decode %v into a primitive.CodeWithScope", vrType) + } + if err != nil { + return emptyValue, err + } + + return reflect.ValueOf(cws), nil +} + +// CodeWithScopeDecodeValue is the ValueDecoderFunc for CodeWithScope. +func (dvd DefaultValueDecoders) CodeWithScopeDecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tCodeWithScope { + return ValueDecoderError{Name: "CodeWithScopeDecodeValue", Types: []reflect.Type{tCodeWithScope}, Received: val} + } + + elem, err := dvd.codeWithScopeDecodeType(dc, vr, tCodeWithScope) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +func (dvd DefaultValueDecoders) decodeD(dc DecodeContext, vr bsonrw.ValueReader, _ reflect.Value) ([]reflect.Value, error) { + switch vr.Type() { + case bsontype.Type(0), bsontype.EmbeddedDocument: + default: + return nil, fmt.Errorf("cannot decode %v into a D", vr.Type()) + } + + dr, err := vr.ReadDocument() + if err != nil { + return nil, err + } + + return dvd.decodeElemsFromDocumentReader(dc, dr) +} + +func (DefaultValueDecoders) decodeElemsFromDocumentReader(dc DecodeContext, dr bsonrw.DocumentReader) ([]reflect.Value, error) { + decoder, err := dc.LookupDecoder(tEmpty) + if err != nil { + return nil, err + } + + elems := make([]reflect.Value, 0) + for { + key, vr, err := dr.ReadElement() + if err == bsonrw.ErrEOD { + break + } + if err != nil { + return nil, err + } + + val := reflect.New(tEmpty).Elem() + err = decoder.DecodeValue(dc, vr, val) + if err != nil { + return nil, newDecodeError(key, err) + } + + elems = append(elems, reflect.ValueOf(primitive.E{Key: key, Value: val.Interface()})) + } + + return elems, nil +} diff --git a/mongo/bson/bsoncodec/default_value_decoders_test.go b/mongo/bson/bsoncodec/default_value_decoders_test.go new file mode 100644 index 0000000..0cd41aa --- /dev/null +++ b/mongo/bson/bsoncodec/default_value_decoders_test.go @@ -0,0 +1,3794 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "net/url" + "reflect" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/internal/testutil/assert" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +var ( + defaultTestStructCodec = newDefaultStructCodec() +) + +func TestDefaultValueDecoders(t *testing.T) { + var dvd DefaultValueDecoders + var wrong = func(string, string) string { return "wrong" } + + type mybool bool + type myint8 int8 + type myint16 int16 + type myint32 int32 + type myint64 int64 + type myint int + type myuint8 uint8 + type myuint16 uint16 + type myuint32 uint32 + type myuint64 uint64 + type myuint uint + type myfloat32 float32 + type myfloat64 float64 + type mystring string + type mystruct struct{} + + const cansetreflectiontest = "cansetreflectiontest" + const cansettest = "cansettest" + + now := time.Now().Truncate(time.Millisecond) + d128 := primitive.NewDecimal128(12345, 67890) + var pbool = func(b bool) *bool { return &b } + var pi32 = func(i32 int32) *int32 { return &i32 } + var pi64 = func(i64 int64) *int64 { return &i64 } + + type subtest struct { + name string + val interface{} + dctx *DecodeContext + llvrw *bsonrwtest.ValueReaderWriter + invoke bsonrwtest.Invoked + err error + } + + testCases := []struct { + name string + vd ValueDecoder + subtests []subtest + }{ + { + "BooleanDecodeValue", + ValueDecoderFunc(dvd.BooleanDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Boolean}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "BooleanDecodeValue", Kinds: []reflect.Kind{reflect.Bool}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not boolean", + bool(false), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a boolean", bsontype.String), + }, + { + "fast path", + bool(true), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Boolean, Return: bool(true)}, + bsonrwtest.ReadBoolean, + nil, + }, + { + "reflection path", + mybool(true), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Boolean, Return: bool(true)}, + bsonrwtest.ReadBoolean, + nil, + }, + { + "reflection path error", + mybool(true), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Boolean, Return: bool(true), Err: errors.New("ReadBoolean Error"), ErrAfter: bsonrwtest.ReadBoolean}, + bsonrwtest.ReadBoolean, errors.New("ReadBoolean Error"), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Boolean}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "BooleanDecodeValue", Kinds: []reflect.Kind{reflect.Bool}}, + }, + { + "decode null", + mybool(false), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + mybool(false), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "IntDecodeValue", + ValueDecoderFunc(dvd.IntDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, + bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "type not int32/int64", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into an integer type", bsontype.String), + }, + { + "ReadInt32 error", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0), Err: errors.New("ReadInt32 error"), ErrAfter: bsonrwtest.ReadInt32}, + bsonrwtest.ReadInt32, + errors.New("ReadInt32 error"), + }, + { + "ReadInt64 error", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(0), Err: errors.New("ReadInt64 error"), ErrAfter: bsonrwtest.ReadInt64}, + bsonrwtest.ReadInt64, + errors.New("ReadInt64 error"), + }, + { + "ReadDouble error", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0), Err: errors.New("ReadDouble error"), ErrAfter: bsonrwtest.ReadDouble}, + bsonrwtest.ReadDouble, + errors.New("ReadDouble error"), + }, + { + "ReadDouble", int64(3), &DecodeContext{}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.00)}, bsonrwtest.ReadDouble, + nil, + }, + { + "ReadDouble (truncate)", int64(3), &DecodeContext{Truncate: true}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + nil, + }, + { + "ReadDouble (no truncate)", int64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + errCannotTruncate, + }, + { + "ReadDouble overflows int64", int64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: math.MaxFloat64}, bsonrwtest.ReadDouble, + fmt.Errorf("%g overflows int64", math.MaxFloat64), + }, + {"int8/fast path", int8(127), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(127)}, bsonrwtest.ReadInt32, nil}, + {"int16/fast path", int16(32676), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(32676)}, bsonrwtest.ReadInt32, nil}, + {"int32/fast path", int32(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1234)}, bsonrwtest.ReadInt32, nil}, + {"int64/fast path", int64(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1234)}, bsonrwtest.ReadInt64, nil}, + {"int/fast path", int(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1234)}, bsonrwtest.ReadInt64, nil}, + { + "int8/fast path - nil", (*int8)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf((*int8)(nil)), + }, + }, + { + "int16/fast path - nil", (*int16)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf((*int16)(nil)), + }, + }, + { + "int32/fast path - nil", (*int32)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf((*int32)(nil)), + }, + }, + { + "int64/fast path - nil", (*int64)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf((*int64)(nil)), + }, + }, + { + "int/fast path - nil", (*int)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf((*int)(nil)), + }, + }, + { + "int8/fast path - overflow", int8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(129)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int8", 129), + }, + { + "int16/fast path - overflow", int16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(32768)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int16", 32768), + }, + { + "int32/fast path - overflow", int32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(2147483648)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows int32", int64(2147483648)), + }, + { + "int8/fast path - overflow (negative)", int8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-129)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int8", -129), + }, + { + "int16/fast path - overflow (negative)", int16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-32769)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int16", -32769), + }, + { + "int32/fast path - overflow (negative)", int32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-2147483649)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows int32", int64(-2147483649)), + }, + { + "int8/reflection path", myint8(127), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(127)}, bsonrwtest.ReadInt32, + nil, + }, + { + "int16/reflection path", myint16(255), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(255)}, bsonrwtest.ReadInt32, + nil, + }, + { + "int32/reflection path", myint32(511), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(511)}, bsonrwtest.ReadInt32, + nil, + }, + { + "int64/reflection path", myint64(1023), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1023)}, bsonrwtest.ReadInt32, + nil, + }, + { + "int/reflection path", myint(2047), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(2047)}, bsonrwtest.ReadInt32, + nil, + }, + { + "int8/reflection path - overflow", myint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(129)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int8", 129), + }, + { + "int16/reflection path - overflow", myint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(32768)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int16", 32768), + }, + { + "int32/reflection path - overflow", myint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(2147483648)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows int32", int64(2147483648)), + }, + { + "int8/reflection path - overflow (negative)", myint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-129)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int8", -129), + }, + { + "int16/reflection path - overflow (negative)", myint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-32769)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows int16", -32769), + }, + { + "int32/reflection path - overflow (negative)", myint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-2147483649)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows int32", int64(-2147483649)), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "IntDecodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + }, + }, + { + "decode null", + myint(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + myint(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "defaultUIntCodec.DecodeValue", + defaultUIntCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, + bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "type not int32/int64", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into an integer type", bsontype.String), + }, + { + "ReadInt32 error", + uint(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0), Err: errors.New("ReadInt32 error"), ErrAfter: bsonrwtest.ReadInt32}, + bsonrwtest.ReadInt32, + errors.New("ReadInt32 error"), + }, + { + "ReadInt64 error", + uint(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(0), Err: errors.New("ReadInt64 error"), ErrAfter: bsonrwtest.ReadInt64}, + bsonrwtest.ReadInt64, + errors.New("ReadInt64 error"), + }, + { + "ReadDouble error", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0), Err: errors.New("ReadDouble error"), ErrAfter: bsonrwtest.ReadDouble}, + bsonrwtest.ReadDouble, + errors.New("ReadDouble error"), + }, + { + "ReadDouble", uint64(3), &DecodeContext{}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.00)}, bsonrwtest.ReadDouble, + nil, + }, + { + "ReadDouble (truncate)", uint64(3), &DecodeContext{Truncate: true}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + nil, + }, + { + "ReadDouble (no truncate)", uint64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + errCannotTruncate, + }, + { + "ReadDouble overflows int64", uint64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: math.MaxFloat64}, bsonrwtest.ReadDouble, + fmt.Errorf("%g overflows int64", math.MaxFloat64), + }, + {"uint8/fast path", uint8(127), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(127)}, bsonrwtest.ReadInt32, nil}, + {"uint16/fast path", uint16(255), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(255)}, bsonrwtest.ReadInt32, nil}, + {"uint32/fast path", uint32(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1234)}, bsonrwtest.ReadInt32, nil}, + {"uint64/fast path", uint64(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1234)}, bsonrwtest.ReadInt64, nil}, + {"uint/fast path", uint(1234), nil, &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1234)}, bsonrwtest.ReadInt64, nil}, + { + "uint8/fast path - nil", (*uint8)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf((*uint8)(nil)), + }, + }, + { + "uint16/fast path - nil", (*uint16)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf((*uint16)(nil)), + }, + }, + { + "uint32/fast path - nil", (*uint32)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf((*uint32)(nil)), + }, + }, + { + "uint64/fast path - nil", (*uint64)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf((*uint64)(nil)), + }, + }, + { + "uint/fast path - nil", (*uint)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, bsonrwtest.ReadInt32, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf((*uint)(nil)), + }, + }, + { + "uint8/fast path - overflow", uint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1 << 8)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint8", 1<<8), + }, + { + "uint16/fast path - overflow", uint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1 << 16)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint16", 1<<16), + }, + { + "uint32/fast path - overflow", uint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1 << 32)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint32", int64(1<<32)), + }, + { + "uint8/fast path - overflow (negative)", uint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-1)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint8", -1), + }, + { + "uint16/fast path - overflow (negative)", uint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-1)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint16", -1), + }, + { + "uint32/fast path - overflow (negative)", uint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint32", -1), + }, + { + "uint64/fast path - overflow (negative)", uint64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint64", -1), + }, + { + "uint/fast path - overflow (negative)", uint(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint", -1), + }, + { + "uint8/reflection path", myuint8(127), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(127)}, bsonrwtest.ReadInt32, + nil, + }, + { + "uint16/reflection path", myuint16(255), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(255)}, bsonrwtest.ReadInt32, + nil, + }, + { + "uint32/reflection path", myuint32(511), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(511)}, bsonrwtest.ReadInt32, + nil, + }, + { + "uint64/reflection path", myuint64(1023), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1023)}, bsonrwtest.ReadInt32, + nil, + }, + { + "uint/reflection path", myuint(2047), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(2047)}, bsonrwtest.ReadInt32, + nil, + }, + { + "uint8/reflection path - overflow", myuint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1 << 8)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint8", 1<<8), + }, + { + "uint16/reflection path - overflow", myuint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(1 << 16)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint16", 1<<16), + }, + { + "uint32/reflection path - overflow", myuint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1 << 32)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint32", int64(1<<32)), + }, + { + "uint8/reflection path - overflow (negative)", myuint8(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-1)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint8", -1), + }, + { + "uint16/reflection path - overflow (negative)", myuint16(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(-1)}, bsonrwtest.ReadInt32, + fmt.Errorf("%d overflows uint16", -1), + }, + { + "uint32/reflection path - overflow (negative)", myuint32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint32", -1), + }, + { + "uint64/reflection path - overflow (negative)", myuint64(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint64", -1), + }, + { + "uint/reflection path - overflow (negative)", myuint(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(-1)}, bsonrwtest.ReadInt64, + fmt.Errorf("%d overflows uint", -1), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0)}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + }, + }, + }, + }, + { + "FloatDecodeValue", + ValueDecoderFunc(dvd.FloatDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0)}, + bsonrwtest.ReadDouble, + ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "type not double", + 0, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a float32 or float64 type", bsontype.String), + }, + { + "ReadDouble error", + float64(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0), Err: errors.New("ReadDouble error"), ErrAfter: bsonrwtest.ReadDouble}, + bsonrwtest.ReadDouble, + errors.New("ReadDouble error"), + }, + { + "ReadInt32 error", + float64(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(0), Err: errors.New("ReadInt32 error"), ErrAfter: bsonrwtest.ReadInt32}, + bsonrwtest.ReadInt32, + errors.New("ReadInt32 error"), + }, + { + "ReadInt64 error", + float64(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(0), Err: errors.New("ReadInt64 error"), ErrAfter: bsonrwtest.ReadInt64}, + bsonrwtest.ReadInt64, + errors.New("ReadInt64 error"), + }, + { + "float64/int32", float32(32.0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(32)}, bsonrwtest.ReadInt32, + nil, + }, + { + "float64/int64", float32(64.0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(64)}, bsonrwtest.ReadInt64, + nil, + }, + { + "float32/fast path (equal)", float32(3.0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.0)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float64/fast path", float64(3.14159), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14159)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float32/fast path (truncate)", float32(3.14), &DecodeContext{Truncate: true}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float32/fast path (no truncate)", float32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + errCannotTruncate, + }, + { + "float32/fast path - nil", (*float32)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0)}, bsonrwtest.ReadDouble, + ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: reflect.ValueOf((*float32)(nil)), + }, + }, + { + "float64/fast path - nil", (*float64)(nil), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0)}, bsonrwtest.ReadDouble, + ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: reflect.ValueOf((*float64)(nil)), + }, + }, + { + "float32/reflection path (equal)", myfloat32(3.0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.0)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float64/reflection path", myfloat64(3.14159), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14159)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float32/reflection path (truncate)", myfloat32(3.14), &DecodeContext{Truncate: true}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + nil, + }, + { + "float32/reflection path (no truncate)", myfloat32(0), nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14)}, bsonrwtest.ReadDouble, + errCannotTruncate, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(0)}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "FloatDecodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + }, + }, + }, + }, + { + "defaultTimeCodec.DecodeValue", + defaultTimeCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: int64(0)}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "TimeDecodeValue", Types: []reflect.Type{tTime}, Received: reflect.ValueOf(wrong)}, + }, + { + "ReadDateTime error", + time.Time{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: int64(0), Err: errors.New("ReadDateTime error"), ErrAfter: bsonrwtest.ReadDateTime}, + bsonrwtest.ReadDateTime, + errors.New("ReadDateTime error"), + }, + { + "time.Time", + now, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: now.UnixNano() / int64(time.Millisecond)}, + bsonrwtest.ReadDateTime, + nil, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: int64(0)}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "TimeDecodeValue", Types: []reflect.Type{tTime}}, + }, + { + "decode null", + time.Time{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + time.Time{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "defaultMapCodec.DecodeValue", + defaultMapCodec, + []subtest{ + { + "wrong kind", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "MapDecodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: reflect.ValueOf(wrong)}, + }, + { + "wrong kind (non-string key)", + map[bool]interface{}{}, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.ReadElement, + fmt.Errorf("unsupported key type: %T", false), + }, + { + "ReadDocument Error", + make(map[string]interface{}), + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("rd error"), ErrAfter: bsonrwtest.ReadDocument}, + bsonrwtest.ReadDocument, + errors.New("rd error"), + }, + { + "Lookup Error", + map[string]string{}, + &DecodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.ReadDocument, + ErrNoDecoder{Type: reflect.TypeOf("")}, + }, + { + "ReadElement Error", + make(map[string]interface{}), + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("re error"), ErrAfter: bsonrwtest.ReadElement}, + bsonrwtest.ReadElement, + errors.New("re error"), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "MapDecodeValue", Kinds: []reflect.Kind{reflect.Map}}, + }, + { + "wrong BSON type", + map[string]interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + errors.New("cannot decode string into a map[string]interface {}"), + }, + { + "decode null", + (map[string]interface{})(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + (map[string]interface{})(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "ArrayDecodeValue", + ValueDecoderFunc(dvd.ArrayDecodeValue), + []subtest{ + { + "wrong kind", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ArrayDecodeValue", Kinds: []reflect.Kind{reflect.Array}, Received: reflect.ValueOf(wrong)}, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ArrayDecodeValue", Kinds: []reflect.Kind{reflect.Array}}, + }, + { + "Not Type Array", + [1]interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + errors.New("cannot decode string into an array"), + }, + { + "ReadArray Error", + [1]interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ra error"), ErrAfter: bsonrwtest.ReadArray, BSONType: bsontype.Array}, + bsonrwtest.ReadArray, + errors.New("ra error"), + }, + { + "Lookup Error", + [1]string{}, + &DecodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + bsonrwtest.ReadArray, + ErrNoDecoder{Type: reflect.TypeOf("")}, + }, + { + "ReadValue Error", + [1]string{}, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("rv error"), ErrAfter: bsonrwtest.ReadValue, BSONType: bsontype.Array}, + bsonrwtest.ReadValue, + errors.New("rv error"), + }, + { + "DecodeValue Error", + [1]string{}, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + bsonrwtest.ReadValue, + &DecodeError{keys: []string{"0"}, wrapped: errors.New("cannot decode array into a string type")}, + }, + { + "Document but not D", + [1]string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Type(0)}, + bsonrwtest.Nothing, + errors.New("cannot decode document into [1]string"), + }, + { + "EmbeddedDocument but not D", + [1]string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.EmbeddedDocument}, + bsonrwtest.Nothing, + errors.New("cannot decode document into [1]string"), + }, + { + "decode null", + [1]string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + [1]string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "defaultSliceCodec.DecodeValue", + defaultSliceCodec, + []subtest{ + { + "wrong kind", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "SliceDecodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: reflect.ValueOf(wrong)}, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "SliceDecodeValue", Kinds: []reflect.Kind{reflect.Slice}}, + }, + { + "Not Type Array", + []interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32}, + bsonrwtest.Nothing, + errors.New("cannot decode 32-bit integer into a slice"), + }, + { + "ReadArray Error", + []interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ra error"), ErrAfter: bsonrwtest.ReadArray, BSONType: bsontype.Array}, + bsonrwtest.ReadArray, + errors.New("ra error"), + }, + { + "Lookup Error", + []string{}, + &DecodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + bsonrwtest.ReadArray, + ErrNoDecoder{Type: reflect.TypeOf("")}, + }, + { + "ReadValue Error", + []string{}, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("rv error"), ErrAfter: bsonrwtest.ReadValue, BSONType: bsontype.Array}, + bsonrwtest.ReadValue, + errors.New("rv error"), + }, + { + "DecodeValue Error", + []string{}, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + bsonrwtest.ReadValue, + &DecodeError{keys: []string{"0"}, wrapped: errors.New("cannot decode array into a string type")}, + }, + { + "Document but not D", + []string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Type(0)}, + bsonrwtest.Nothing, + errors.New("cannot decode document into []string"), + }, + { + "EmbeddedDocument but not D", + []string{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.EmbeddedDocument}, + bsonrwtest.Nothing, + errors.New("cannot decode document into []string"), + }, + { + "decode null", + ([]string)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + ([]string)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "ObjectIDDecodeValue", + ValueDecoderFunc(dvd.ObjectIDDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ObjectIDDecodeValue", Types: []reflect.Type{tOID}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not objectID", + primitive.ObjectID{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into an ObjectID", bsontype.Int32), + }, + { + "ReadObjectID Error", + primitive.ObjectID{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID, Err: errors.New("roid error"), ErrAfter: bsonrwtest.ReadObjectID}, + bsonrwtest.ReadObjectID, + errors.New("roid error"), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID, Return: primitive.ObjectID{}}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ObjectIDDecodeValue", Types: []reflect.Type{tOID}}, + }, + { + "success", + primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.ObjectID, + Return: primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + }, + bsonrwtest.ReadObjectID, + nil, + }, + { + "success/string", + primitive.ObjectID{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.String, + Return: "0123456789ab", + }, + bsonrwtest.ReadString, + nil, + }, + { + "success/string-hex", + primitive.ObjectID{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.String, + Return: "303132333435363738396162", + }, + bsonrwtest.ReadString, + nil, + }, + { + "decode null", + primitive.ObjectID{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.ObjectID{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "Decimal128DecodeValue", + ValueDecoderFunc(dvd.Decimal128DecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Decimal128}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "Decimal128DecodeValue", Types: []reflect.Type{tDecimal}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not decimal128", + primitive.Decimal128{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a primitive.Decimal128", bsontype.String), + }, + { + "ReadDecimal128 Error", + primitive.Decimal128{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Decimal128, Err: errors.New("rd128 error"), ErrAfter: bsonrwtest.ReadDecimal128}, + bsonrwtest.ReadDecimal128, + errors.New("rd128 error"), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Decimal128, Return: d128}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "Decimal128DecodeValue", Types: []reflect.Type{tDecimal}}, + }, + { + "success", + d128, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Decimal128, Return: d128}, + bsonrwtest.ReadDecimal128, + nil, + }, + { + "decode null", + primitive.Decimal128{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.Decimal128{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "JSONNumberDecodeValue", + ValueDecoderFunc(dvd.JSONNumberDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "JSONNumberDecodeValue", Types: []reflect.Type{tJSONNumber}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not double/int32/int64", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a json.Number", bsontype.String), + }, + { + "ReadDouble Error", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Err: errors.New("rd error"), ErrAfter: bsonrwtest.ReadDouble}, + bsonrwtest.ReadDouble, + errors.New("rd error"), + }, + { + "ReadInt32 Error", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Err: errors.New("ri32 error"), ErrAfter: bsonrwtest.ReadInt32}, + bsonrwtest.ReadInt32, + errors.New("ri32 error"), + }, + { + "ReadInt64 Error", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Err: errors.New("ri64 error"), ErrAfter: bsonrwtest.ReadInt64}, + bsonrwtest.ReadInt64, + errors.New("ri64 error"), + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID, Return: primitive.ObjectID{}}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "JSONNumberDecodeValue", Types: []reflect.Type{tJSONNumber}}, + }, + { + "success/double", + json.Number("3.14159"), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14159)}, + bsonrwtest.ReadDouble, + nil, + }, + { + "success/int32", + json.Number("12345"), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32, Return: int32(12345)}, + bsonrwtest.ReadInt32, + nil, + }, + { + "success/int64", + json.Number("1234567890"), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: int64(1234567890)}, + bsonrwtest.ReadInt64, + nil, + }, + { + "decode null", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + json.Number(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "URLDecodeValue", + ValueDecoderFunc(dvd.URLDecodeValue), + []subtest{ + { + "wrong type", + url.URL{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a *url.URL", bsontype.Int32), + }, + { + "type not *url.URL", + int64(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: "http://example.com"}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "URLDecodeValue", Types: []reflect.Type{tURL}, Received: reflect.ValueOf(int64(0))}, + }, + { + "ReadString error", + url.URL{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Err: errors.New("rs error"), ErrAfter: bsonrwtest.ReadString}, + bsonrwtest.ReadString, + errors.New("rs error"), + }, + { + "url.Parse error", + url.URL{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: "not-valid-%%%%://"}, + bsonrwtest.ReadString, + &url.Error{ + Op: "parse", + URL: "not-valid-%%%%://", + Err: errors.New("first path segment in URL cannot contain colon"), + }, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: "http://example.com"}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "URLDecodeValue", Types: []reflect.Type{tURL}}, + }, + { + "url.URL", + url.URL{Scheme: "http", Host: "example.com"}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: "http://example.com"}, + bsonrwtest.ReadString, + nil, + }, + { + "decode null", + url.URL{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + url.URL{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "defaultByteSliceCodec.DecodeValue", + defaultByteSliceCodec, + []subtest{ + { + "wrong type", + []byte{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a []byte", bsontype.Int32), + }, + { + "type not []byte", + int64(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Binary, Return: bsoncore.Value{Type: bsontype.Binary}}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ByteSliceDecodeValue", Types: []reflect.Type{tByteSlice}, Received: reflect.ValueOf(int64(0))}, + }, + { + "ReadBinary error", + []byte{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Binary, Err: errors.New("rb error"), ErrAfter: bsonrwtest.ReadBinary}, + bsonrwtest.ReadBinary, + errors.New("rb error"), + }, + { + "incorrect subtype", + []byte{}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.Binary, + Return: bsoncore.Value{ + Type: bsontype.Binary, + Data: bsoncore.AppendBinary(nil, 0xFF, []byte{0x01, 0x02, 0x03}), + }, + }, + bsonrwtest.ReadBinary, + decodeBinaryError{subtype: byte(0xFF), typeName: "[]byte"}, + }, + { + "can set false", + cansettest, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Binary, Return: bsoncore.AppendBinary(nil, 0x00, []byte{0x01, 0x02, 0x03})}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "ByteSliceDecodeValue", Types: []reflect.Type{tByteSlice}}, + }, + { + "decode null", + ([]byte)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + ([]byte)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "defaultStringCodec.DecodeValue", + defaultStringCodec, + []subtest{ + { + "symbol", + "var hello = 'world';", + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Symbol, Return: "var hello = 'world';"}, + bsonrwtest.ReadSymbol, + nil, + }, + { + "decode null", + "", + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + "", + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "ValueUnmarshalerDecodeValue", + ValueDecoderFunc(dvd.ValueUnmarshalerDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "ValueUnmarshalerDecodeValue", + Types: []reflect.Type{tValueUnmarshaler}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "copy error", + &testValueUnmarshaler{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Err: errors.New("copy error"), ErrAfter: bsonrwtest.ReadString}, + bsonrwtest.ReadString, + errors.New("copy error"), + }, + { + "ValueUnmarshaler", + &testValueUnmarshaler{t: bsontype.String, val: bsoncore.AppendString(nil, "hello, world")}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: "hello, world"}, + bsonrwtest.ReadString, + nil, + }, + }, + }, + { + "UnmarshalerDecodeValue", + ValueDecoderFunc(dvd.UnmarshalerDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueDecoderError{Name: "UnmarshalerDecodeValue", Types: []reflect.Type{tUnmarshaler}, Received: reflect.ValueOf(wrong)}, + }, + { + "copy error", + &testUnmarshaler{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Err: errors.New("copy error"), ErrAfter: bsonrwtest.ReadString}, + bsonrwtest.ReadString, + errors.New("copy error"), + }, + { + "Unmarshaler", + testUnmarshaler{Val: bsoncore.AppendDouble(nil, 3.14159)}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double, Return: float64(3.14159)}, + bsonrwtest.ReadDouble, + nil, + }, + }, + }, + { + "PointerCodec.DecodeValue", + NewPointerCodec(), + []subtest{ + { + "not valid", nil, nil, nil, bsonrwtest.Nothing, + ValueDecoderError{Name: "PointerCodec.DecodeValue", Kinds: []reflect.Kind{reflect.Ptr}, Received: reflect.Value{}}, + }, + { + "can set", cansettest, nil, nil, bsonrwtest.Nothing, + ValueDecoderError{Name: "PointerCodec.DecodeValue", Kinds: []reflect.Kind{reflect.Ptr}}, + }, + { + "No Decoder", &wrong, &DecodeContext{Registry: buildDefaultRegistry()}, nil, bsonrwtest.Nothing, + ErrNoDecoder{Type: reflect.TypeOf(wrong)}, + }, + { + "decode null", + (*mystruct)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + (*mystruct)(nil), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "BinaryDecodeValue", + ValueDecoderFunc(dvd.BinaryDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "BinaryDecodeValue", Types: []reflect.Type{tBinary}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not binary", + primitive.Binary{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a Binary", bsontype.String), + }, + { + "ReadBinary Error", + primitive.Binary{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Binary, Err: errors.New("rb error"), ErrAfter: bsonrwtest.ReadBinary}, + bsonrwtest.ReadBinary, + errors.New("rb error"), + }, + { + "Binary/success", + primitive.Binary{Data: []byte{0x01, 0x02, 0x03}, Subtype: 0xFF}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.Binary, + Return: bsoncore.Value{ + Type: bsontype.Binary, + Data: bsoncore.AppendBinary(nil, 0xFF, []byte{0x01, 0x02, 0x03}), + }, + }, + bsonrwtest.ReadBinary, + nil, + }, + { + "decode null", + primitive.Binary{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.Binary{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "UndefinedDecodeValue", + ValueDecoderFunc(dvd.UndefinedDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "UndefinedDecodeValue", Types: []reflect.Type{tUndefined}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not undefined", + primitive.Undefined{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into an Undefined", bsontype.String), + }, + { + "ReadUndefined Error", + primitive.Undefined{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined, Err: errors.New("ru error"), ErrAfter: bsonrwtest.ReadUndefined}, + bsonrwtest.ReadUndefined, + errors.New("ru error"), + }, + { + "ReadUndefined/success", + primitive.Undefined{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + { + "decode null", + primitive.Undefined{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + }, + }, + { + "DateTimeDecodeValue", + ValueDecoderFunc(dvd.DateTimeDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "DateTimeDecodeValue", Types: []reflect.Type{tDateTime}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not datetime", + primitive.DateTime(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a DateTime", bsontype.String), + }, + { + "ReadDateTime Error", + primitive.DateTime(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Err: errors.New("rdt error"), ErrAfter: bsonrwtest.ReadDateTime}, + bsonrwtest.ReadDateTime, + errors.New("rdt error"), + }, + { + "success", + primitive.DateTime(1234567890), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: int64(1234567890)}, + bsonrwtest.ReadDateTime, + nil, + }, + { + "decode null", + primitive.DateTime(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.DateTime(0), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "NullDecodeValue", + ValueDecoderFunc(dvd.NullDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "NullDecodeValue", Types: []reflect.Type{tNull}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not null", + primitive.Null{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a Null", bsontype.String), + }, + { + "ReadNull Error", + primitive.Null{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null, Err: errors.New("rn error"), ErrAfter: bsonrwtest.ReadNull}, + bsonrwtest.ReadNull, + errors.New("rn error"), + }, + { + "success", + primitive.Null{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + }, + }, + { + "RegexDecodeValue", + ValueDecoderFunc(dvd.RegexDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Regex}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "RegexDecodeValue", Types: []reflect.Type{tRegex}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not regex", + primitive.Regex{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a Regex", bsontype.String), + }, + { + "ReadRegex Error", + primitive.Regex{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Regex, Err: errors.New("rr error"), ErrAfter: bsonrwtest.ReadRegex}, + bsonrwtest.ReadRegex, + errors.New("rr error"), + }, + { + "success", + primitive.Regex{Pattern: "foo", Options: "bar"}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.Regex, + Return: bsoncore.Value{ + Type: bsontype.Regex, + Data: bsoncore.AppendRegex(nil, "foo", "bar"), + }, + }, + bsonrwtest.ReadRegex, + nil, + }, + { + "decode null", + primitive.Regex{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.Regex{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "DBPointerDecodeValue", + ValueDecoderFunc(dvd.DBPointerDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DBPointer}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "DBPointerDecodeValue", Types: []reflect.Type{tDBPointer}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not dbpointer", + primitive.DBPointer{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a DBPointer", bsontype.String), + }, + { + "ReadDBPointer Error", + primitive.DBPointer{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DBPointer, Err: errors.New("rdbp error"), ErrAfter: bsonrwtest.ReadDBPointer}, + bsonrwtest.ReadDBPointer, + errors.New("rdbp error"), + }, + { + "success", + primitive.DBPointer{ + DB: "foobar", + Pointer: primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + }, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.DBPointer, + Return: bsoncore.Value{ + Type: bsontype.DBPointer, + Data: bsoncore.AppendDBPointer( + nil, "foobar", primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + ), + }, + }, + bsonrwtest.ReadDBPointer, + nil, + }, + { + "decode null", + primitive.DBPointer{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.DBPointer{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "TimestampDecodeValue", + ValueDecoderFunc(dvd.TimestampDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Timestamp}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "TimestampDecodeValue", Types: []reflect.Type{tTimestamp}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not timestamp", + primitive.Timestamp{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a Timestamp", bsontype.String), + }, + { + "ReadTimestamp Error", + primitive.Timestamp{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Timestamp, Err: errors.New("rt error"), ErrAfter: bsonrwtest.ReadTimestamp}, + bsonrwtest.ReadTimestamp, + errors.New("rt error"), + }, + { + "success", + primitive.Timestamp{T: 12345, I: 67890}, + nil, + &bsonrwtest.ValueReaderWriter{ + BSONType: bsontype.Timestamp, + Return: bsoncore.Value{ + Type: bsontype.Timestamp, + Data: bsoncore.AppendTimestamp(nil, 12345, 67890), + }, + }, + bsonrwtest.ReadTimestamp, + nil, + }, + { + "decode null", + primitive.Timestamp{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.Timestamp{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "MinKeyDecodeValue", + ValueDecoderFunc(dvd.MinKeyDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MinKey}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "MinKeyDecodeValue", Types: []reflect.Type{tMinKey}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not null", + primitive.MinKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a MinKey", bsontype.String), + }, + { + "ReadMinKey Error", + primitive.MinKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MinKey, Err: errors.New("rn error"), ErrAfter: bsonrwtest.ReadMinKey}, + bsonrwtest.ReadMinKey, + errors.New("rn error"), + }, + { + "success", + primitive.MinKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MinKey}, + bsonrwtest.ReadMinKey, + nil, + }, + { + "decode null", + primitive.MinKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.MinKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "MaxKeyDecodeValue", + ValueDecoderFunc(dvd.MaxKeyDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MaxKey}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "MaxKeyDecodeValue", Types: []reflect.Type{tMaxKey}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not null", + primitive.MaxKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a MaxKey", bsontype.String), + }, + { + "ReadMaxKey Error", + primitive.MaxKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MaxKey, Err: errors.New("rn error"), ErrAfter: bsonrwtest.ReadMaxKey}, + bsonrwtest.ReadMaxKey, + errors.New("rn error"), + }, + { + "success", + primitive.MaxKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.MaxKey}, + bsonrwtest.ReadMaxKey, + nil, + }, + { + "decode null", + primitive.MaxKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.MaxKey{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "JavaScriptDecodeValue", + ValueDecoderFunc(dvd.JavaScriptDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.JavaScript, Return: ""}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "JavaScriptDecodeValue", Types: []reflect.Type{tJavaScript}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not Javascript", + primitive.JavaScript(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a primitive.JavaScript", bsontype.String), + }, + { + "ReadJavascript Error", + primitive.JavaScript(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.JavaScript, Err: errors.New("rjs error"), ErrAfter: bsonrwtest.ReadJavascript}, + bsonrwtest.ReadJavascript, + errors.New("rjs error"), + }, + { + "JavaScript/success", + primitive.JavaScript("var hello = 'world';"), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.JavaScript, Return: "var hello = 'world';"}, + bsonrwtest.ReadJavascript, + nil, + }, + { + "decode null", + primitive.JavaScript(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.JavaScript(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "SymbolDecodeValue", + ValueDecoderFunc(dvd.SymbolDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Symbol, Return: ""}, + bsonrwtest.Nothing, + ValueDecoderError{Name: "SymbolDecodeValue", Types: []reflect.Type{tSymbol}, Received: reflect.ValueOf(wrong)}, + }, + { + "type not Symbol", + primitive.Symbol(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int32}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a primitive.Symbol", bsontype.Int32), + }, + { + "ReadSymbol Error", + primitive.Symbol(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Symbol, Err: errors.New("rjs error"), ErrAfter: bsonrwtest.ReadSymbol}, + bsonrwtest.ReadSymbol, + errors.New("rjs error"), + }, + { + "Symbol/success", + primitive.Symbol("var hello = 'world';"), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Symbol, Return: "var hello = 'world';"}, + bsonrwtest.ReadSymbol, + nil, + }, + { + "decode null", + primitive.Symbol(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.Symbol(""), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "CoreDocumentDecodeValue", + ValueDecoderFunc(dvd.CoreDocumentDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "CoreDocumentDecodeValue", + Types: []reflect.Type{tCoreDocument}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "*bsoncore.Document is nil", + (*bsoncore.Document)(nil), + nil, + nil, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "CoreDocumentDecodeValue", + Types: []reflect.Type{tCoreDocument}, + Received: reflect.ValueOf((*bsoncore.Document)(nil)), + }, + }, + { + "Copy error", + bsoncore.Document{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("copy error"), ErrAfter: bsonrwtest.ReadDocument}, + bsonrwtest.ReadDocument, + errors.New("copy error"), + }, + }, + }, + { + "StructCodec.DecodeValue", + defaultTestStructCodec, + []subtest{ + { + "Not struct", + reflect.New(reflect.TypeOf(struct{ Foo string }{})).Elem().Interface(), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + errors.New("cannot decode string into a struct { Foo string }"), + }, + { + "decode null", + reflect.New(reflect.TypeOf(struct{ Foo string }{})).Elem().Interface(), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + reflect.New(reflect.TypeOf(struct{ Foo string }{})).Elem().Interface(), + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "CodeWithScopeDecodeValue", + ValueDecoderFunc(dvd.CodeWithScopeDecodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.CodeWithScope}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "CodeWithScopeDecodeValue", + Types: []reflect.Type{tCodeWithScope}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "type not codewithscope", + primitive.CodeWithScope{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String}, + bsonrwtest.Nothing, + fmt.Errorf("cannot decode %v into a primitive.CodeWithScope", bsontype.String), + }, + { + "ReadCodeWithScope Error", + primitive.CodeWithScope{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.CodeWithScope, Err: errors.New("rcws error"), ErrAfter: bsonrwtest.ReadCodeWithScope}, + bsonrwtest.ReadCodeWithScope, + errors.New("rcws error"), + }, + { + "decodeDocument Error", + primitive.CodeWithScope{ + Code: "var hello = 'world';", + Scope: primitive.D{{"foo", nil}}, + }, + &DecodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.CodeWithScope, Err: errors.New("dd error"), ErrAfter: bsonrwtest.ReadElement}, + bsonrwtest.ReadElement, + errors.New("dd error"), + }, + { + "decode null", + primitive.CodeWithScope{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Null}, + bsonrwtest.ReadNull, + nil, + }, + { + "decode undefined", + primitive.CodeWithScope{}, + nil, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Undefined}, + bsonrwtest.ReadUndefined, + nil, + }, + }, + }, + { + "CoreArrayDecodeValue", + defaultArrayCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "CoreArrayDecodeValue", + Types: []reflect.Type{tCoreArray}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "*bsoncore.Array is nil", + (*bsoncore.Array)(nil), + nil, + nil, + bsonrwtest.Nothing, + ValueDecoderError{ + Name: "CoreArrayDecodeValue", + Types: []reflect.Type{tCoreArray}, + Received: reflect.ValueOf((*bsoncore.Array)(nil)), + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for _, rc := range tc.subtests { + t.Run(rc.name, func(t *testing.T) { + var dc DecodeContext + if rc.dctx != nil { + dc = *rc.dctx + } + llvrw := new(bsonrwtest.ValueReaderWriter) + if rc.llvrw != nil { + llvrw = rc.llvrw + } + llvrw.T = t + // var got interface{} + if rc.val == cansetreflectiontest { // We're doing a CanSet reflection test + err := tc.vd.DecodeValue(dc, llvrw, reflect.Value{}) + if !compareErrors(err, rc.err) { + t.Errorf("Errors do not match. got %v; want %v", err, rc.err) + } + + val := reflect.New(reflect.TypeOf(rc.val)).Elem() + err = tc.vd.DecodeValue(dc, llvrw, val) + if !compareErrors(err, rc.err) { + t.Errorf("Errors do not match. got %v; want %v", err, rc.err) + } + return + } + if rc.val == cansettest { // We're doing an IsValid and CanSet test + wanterr, ok := rc.err.(ValueDecoderError) + if !ok { + t.Fatalf("Error must be a DecodeValueError, but got a %T", rc.err) + } + + err := tc.vd.DecodeValue(dc, llvrw, reflect.Value{}) + wanterr.Received = reflect.ValueOf(nil) + if !compareErrors(err, wanterr) { + t.Errorf("Errors do not match. got %v; want %v", err, wanterr) + } + + err = tc.vd.DecodeValue(dc, llvrw, reflect.ValueOf(int(12345))) + wanterr.Received = reflect.ValueOf(int(12345)) + if !compareErrors(err, wanterr) { + t.Errorf("Errors do not match. got %v; want %v", err, wanterr) + } + return + } + var val reflect.Value + if rtype := reflect.TypeOf(rc.val); rtype != nil { + val = reflect.New(rtype).Elem() + } + want := rc.val + defer func() { + if err := recover(); err != nil { + fmt.Println(t.Name()) + panic(err) + } + }() + err := tc.vd.DecodeValue(dc, llvrw, val) + if !compareErrors(err, rc.err) { + t.Errorf("Errors do not match. got %v; want %v", err, rc.err) + } + invoked := llvrw.Invoked + if !cmp.Equal(invoked, rc.invoke) { + t.Errorf("Incorrect method invoked. got %v; want %v", invoked, rc.invoke) + } + var got interface{} + if val.IsValid() && val.CanInterface() { + got = val.Interface() + } + if rc.err == nil && !cmp.Equal(got, want, cmp.Comparer(compareDecimal128)) { + t.Errorf("Values do not match. got (%T)%v; want (%T)%v", got, got, want, want) + } + }) + } + }) + } + + t.Run("CodeWithScopeCodec/DecodeValue/success", func(t *testing.T) { + dc := DecodeContext{Registry: buildDefaultRegistry()} + b := bsoncore.BuildDocument(nil, + bsoncore.AppendCodeWithScopeElement( + nil, "foo", "var hello = 'world';", + buildDocument(bsoncore.AppendNullElement(nil, "bar")), + ), + ) + dvr := bsonrw.NewBSONDocumentReader(b) + dr, err := dvr.ReadDocument() + noerr(t, err) + _, vr, err := dr.ReadElement() + noerr(t, err) + + want := primitive.CodeWithScope{ + Code: "var hello = 'world';", + Scope: primitive.D{{"bar", nil}}, + } + val := reflect.New(tCodeWithScope).Elem() + err = dvd.CodeWithScopeDecodeValue(dc, vr, val) + noerr(t, err) + + got := val.Interface().(primitive.CodeWithScope) + if got.Code != want.Code && !cmp.Equal(got.Scope, want.Scope) { + t.Errorf("CodeWithScopes do not match. got %v; want %v", got, want) + } + }) + t.Run("ValueUnmarshalerDecodeValue/UnmarshalBSONValue error", func(t *testing.T) { + var dc DecodeContext + llvrw := &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: string("hello, world!")} + llvrw.T = t + + want := errors.New("ubsonv error") + valUnmarshaler := &testValueUnmarshaler{err: want} + got := dvd.ValueUnmarshalerDecodeValue(dc, llvrw, reflect.ValueOf(valUnmarshaler)) + if !compareErrors(got, want) { + t.Errorf("Errors do not match. got %v; want %v", got, want) + } + }) + t.Run("ValueUnmarshalerDecodeValue/Unaddressable value", func(t *testing.T) { + var dc DecodeContext + llvrw := &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: string("hello, world!")} + llvrw.T = t + + val := reflect.ValueOf(testValueUnmarshaler{}) + want := ValueDecoderError{Name: "ValueUnmarshalerDecodeValue", Types: []reflect.Type{tValueUnmarshaler}, Received: val} + got := dvd.ValueUnmarshalerDecodeValue(dc, llvrw, val) + if !compareErrors(got, want) { + t.Errorf("Errors do not match. got %v; want %v", got, want) + } + }) + + t.Run("SliceCodec/DecodeValue/can't set slice", func(t *testing.T) { + var val []string + want := ValueDecoderError{Name: "SliceDecodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: reflect.ValueOf(val)} + got := dvd.SliceDecodeValue(DecodeContext{}, nil, reflect.ValueOf(val)) + if !compareErrors(got, want) { + t.Errorf("Errors do not match. got %v; want %v", got, want) + } + }) + t.Run("SliceCodec/DecodeValue/too many elements", func(t *testing.T) { + idx, doc := bsoncore.AppendDocumentStart(nil) + aidx, doc := bsoncore.AppendArrayElementStart(doc, "foo") + doc = bsoncore.AppendStringElement(doc, "0", "foo") + doc = bsoncore.AppendStringElement(doc, "1", "bar") + doc, err := bsoncore.AppendArrayEnd(doc, aidx) + noerr(t, err) + doc, err = bsoncore.AppendDocumentEnd(doc, idx) + noerr(t, err) + dvr := bsonrw.NewBSONDocumentReader(doc) + noerr(t, err) + dr, err := dvr.ReadDocument() + noerr(t, err) + _, vr, err := dr.ReadElement() + noerr(t, err) + var val [1]string + want := fmt.Errorf("more elements returned in array than can fit inside %T, got 2 elements", val) + + dc := DecodeContext{Registry: buildDefaultRegistry()} + got := dvd.ArrayDecodeValue(dc, vr, reflect.ValueOf(val)) + if !compareErrors(got, want) { + t.Errorf("Errors do not match. got %v; want %v", got, want) + } + }) + + t.Run("success path", func(t *testing.T) { + oid := primitive.NewObjectID() + oids := []primitive.ObjectID{primitive.NewObjectID(), primitive.NewObjectID(), primitive.NewObjectID()} + var str = new(string) + *str = "bar" + now := time.Now().Truncate(time.Millisecond).UTC() + murl, err := url.Parse("https://mongodb.com/random-url?hello=world") + if err != nil { + t.Errorf("Error parsing URL: %v", err) + t.FailNow() + } + decimal128, err := primitive.ParseDecimal128("1.5e10") + if err != nil { + t.Errorf("Error parsing decimal128: %v", err) + t.FailNow() + } + + testCases := []struct { + name string + value interface{} + b []byte + err error + }{ + { + "map[string]int", + map[string]int32{"foo": 1}, + []byte{ + 0x0E, 0x00, 0x00, 0x00, + 0x10, 'f', 'o', 'o', 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x00, + }, + nil, + }, + { + "map[string]primitive.ObjectID", + map[string]primitive.ObjectID{"foo": oid}, + func() []byte { + idx, doc := bsoncore.AppendDocumentStart(nil) + doc = bsoncore.AppendObjectIDElement(doc, "foo", oid) + doc, _ = bsoncore.AppendDocumentEnd(doc, idx) + return doc + }(), + nil, + }, + { + "map[string][]int32", + map[string][]int32{"Z": {1, 2, 3}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt32Element(doc, "0", 1) + doc = bsoncore.AppendInt32Element(doc, "1", 2) + return bsoncore.AppendInt32Element(doc, "2", 3) + }), + nil, + }, + { + "map[string][]primitive.ObjectID", + map[string][]primitive.ObjectID{"Z": oids}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendObjectIDElement(doc, "0", oids[0]) + doc = bsoncore.AppendObjectIDElement(doc, "1", oids[1]) + return bsoncore.AppendObjectIDElement(doc, "2", oids[2]) + }), + nil, + }, + { + "map[string][]json.Number(int64)", + map[string][]json.Number{"Z": {json.Number("5"), json.Number("10")}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt64Element(doc, "0", 5) + return bsoncore.AppendInt64Element(doc, "1", 10) + }), + nil, + }, + { + "map[string][]json.Number(float64)", + map[string][]json.Number{"Z": {json.Number("5"), json.Number("10.1")}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt64Element(doc, "0", 5) + return bsoncore.AppendDoubleElement(doc, "1", 10.1) + }), + nil, + }, + { + "map[string][]*url.URL", + map[string][]*url.URL{"Z": {murl}}, + buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendStringElement(doc, "0", murl.String()) + }), + nil, + }, + { + "map[string][]primitive.Decimal128", + map[string][]primitive.Decimal128{"Z": {decimal128}}, + buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendDecimal128Element(doc, "0", decimal128) + }), + nil, + }, + { + "map[mystring]interface{}", + map[mystring]interface{}{"pi": 3.14159}, + buildDocument(bsoncore.AppendDoubleElement(nil, "pi", 3.14159)), + nil, + }, + { + "-", + struct { + A string `bson:"-"` + }{ + A: "", + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitempty", + struct { + A string `bson:",omitempty"` + }{ + A: "", + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitempty, empty time", + struct { + A time.Time `bson:",omitempty"` + }{ + A: time.Time{}, + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "no private fields", + noPrivateFields{a: "should be empty"}, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "minsize", + struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline", + struct { + Foo struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Foo: struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline struct pointer", + struct { + Foo *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + Bar *struct { + B int64 + } `bson:",inline"` + }{ + Foo: &struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + Bar: nil, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "nested inline struct pointer", + struct { + Foo *struct { + Bar *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + } `bson:",inline"` + }{ + Foo: &struct { + Bar *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Bar: &struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + }, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline nil struct pointer", + struct { + Foo *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Foo: nil, + }, + buildDocument([]byte{}), + nil, + }, + { + "inline overwrite", + struct { + Foo struct { + A int32 + B string + } `bson:",inline"` + A int64 + }{ + Foo: struct { + A int32 + B string + }{ + A: 0, + B: "foo", + }, + A: 54321, + }, + buildDocument(func(doc []byte) []byte { + doc = bsoncore.AppendStringElement(doc, "b", "foo") + doc = bsoncore.AppendInt64Element(doc, "a", 54321) + return doc + }(nil)), + nil, + }, + { + "inline overwrite with nested structs", + struct { + Foo struct { + A int32 + } `bson:",inline"` + Bar struct { + A int32 + } `bson:",inline"` + A int64 + }{ + Foo: struct { + A int32 + }{}, + Bar: struct { + A int32 + }{}, + A: 54321, + }, + buildDocument(bsoncore.AppendInt64Element(nil, "a", 54321)), + nil, + }, + { + "inline map", + struct { + Foo map[string]string `bson:",inline"` + }{ + Foo: map[string]string{"foo": "bar"}, + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "alternate name bson:name", + struct { + A string `bson:"foo"` + }{ + A: "bar", + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "alternate name", + struct { + A string `bson:"foo"` + }{ + A: "bar", + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "inline, omitempty", + struct { + A string + Foo zeroTest `bson:"omitempty,inline"` + }{ + A: "bar", + Foo: zeroTest{true}, + }, + buildDocument(bsoncore.AppendStringElement(nil, "a", "bar")), + nil, + }, + { + "struct{}", + struct { + A bool + B int32 + C int64 + D uint16 + E uint64 + F float64 + G string + H map[string]string + I []byte + K [2]string + L struct { + M string + } + Q primitive.ObjectID + T []struct{} + Y json.Number + Z time.Time + AA json.Number + AB *url.URL + AC primitive.Decimal128 + AD *time.Time + AE *testValueUnmarshaler + AF *bool + AG *bool + AH *int32 + AI *int64 + AJ *primitive.ObjectID + AK *primitive.ObjectID + AL testValueUnmarshaler + AM interface{} + AN interface{} + AO interface{} + AP primitive.D + AQ primitive.A + AR [2]primitive.E + AS []byte + AT map[string]interface{} + AU primitive.CodeWithScope + AV primitive.M + AW primitive.D + AX map[string]interface{} + AY []primitive.E + AZ interface{} + }{ + A: true, + B: 123, + C: 456, + D: 789, + E: 101112, + F: 3.14159, + G: "Hello, world", + H: map[string]string{"foo": "bar"}, + I: []byte{0x01, 0x02, 0x03}, + K: [2]string{"baz", "qux"}, + L: struct { + M string + }{ + M: "foobar", + }, + Q: oid, + T: nil, + Y: json.Number("5"), + Z: now, + AA: json.Number("10.1"), + AB: murl, + AC: decimal128, + AD: &now, + AE: &testValueUnmarshaler{t: bsontype.String, val: bsoncore.AppendString(nil, "hello, world!")}, + AF: func(b bool) *bool { return &b }(true), + AG: nil, + AH: func(i32 int32) *int32 { return &i32 }(12345), + AI: func(i64 int64) *int64 { return &i64 }(1234567890), + AJ: &oid, + AK: nil, + AL: testValueUnmarshaler{t: bsontype.String, val: bsoncore.AppendString(nil, "hello, world!")}, + AM: "hello, world", + AN: int32(12345), + AO: oid, + AP: primitive.D{{"foo", "bar"}}, + AQ: primitive.A{"foo", "bar"}, + AR: [2]primitive.E{{"hello", "world"}, {"pi", 3.14159}}, + AS: nil, + AT: nil, + AU: primitive.CodeWithScope{Code: "var hello = 'world';", Scope: primitive.D{{"pi", 3.14159}}}, + AV: primitive.M{"foo": primitive.M{"bar": "baz"}}, + AW: primitive.D{{"foo", primitive.D{{"bar", "baz"}}}}, + AX: map[string]interface{}{"foo": map[string]interface{}{"bar": "baz"}}, + AY: []primitive.E{{"foo", []primitive.E{{"bar", "baz"}}}}, + AZ: primitive.D{{"foo", primitive.D{{"bar", "baz"}}}}, + }, + buildDocument(func(doc []byte) []byte { + doc = bsoncore.AppendBooleanElement(doc, "a", true) + doc = bsoncore.AppendInt32Element(doc, "b", 123) + doc = bsoncore.AppendInt64Element(doc, "c", 456) + doc = bsoncore.AppendInt32Element(doc, "d", 789) + doc = bsoncore.AppendInt64Element(doc, "e", 101112) + doc = bsoncore.AppendDoubleElement(doc, "f", 3.14159) + doc = bsoncore.AppendStringElement(doc, "g", "Hello, world") + doc = bsoncore.AppendDocumentElement(doc, "h", buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar"))) + doc = bsoncore.AppendBinaryElement(doc, "i", 0x00, []byte{0x01, 0x02, 0x03}) + doc = bsoncore.AppendArrayElement(doc, "k", + buildArray(bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "baz"), "1", "qux")), + ) + doc = bsoncore.AppendDocumentElement(doc, "l", buildDocument(bsoncore.AppendStringElement(nil, "m", "foobar"))) + doc = bsoncore.AppendObjectIDElement(doc, "q", oid) + doc = bsoncore.AppendNullElement(doc, "t") + doc = bsoncore.AppendInt64Element(doc, "y", 5) + doc = bsoncore.AppendDateTimeElement(doc, "z", now.UnixNano()/int64(time.Millisecond)) + doc = bsoncore.AppendDoubleElement(doc, "aa", 10.1) + doc = bsoncore.AppendStringElement(doc, "ab", murl.String()) + doc = bsoncore.AppendDecimal128Element(doc, "ac", decimal128) + doc = bsoncore.AppendDateTimeElement(doc, "ad", now.UnixNano()/int64(time.Millisecond)) + doc = bsoncore.AppendStringElement(doc, "ae", "hello, world!") + doc = bsoncore.AppendBooleanElement(doc, "af", true) + doc = bsoncore.AppendNullElement(doc, "ag") + doc = bsoncore.AppendInt32Element(doc, "ah", 12345) + doc = bsoncore.AppendInt32Element(doc, "ai", 1234567890) + doc = bsoncore.AppendObjectIDElement(doc, "aj", oid) + doc = bsoncore.AppendNullElement(doc, "ak") + doc = bsoncore.AppendStringElement(doc, "al", "hello, world!") + doc = bsoncore.AppendStringElement(doc, "am", "hello, world") + doc = bsoncore.AppendInt32Element(doc, "an", 12345) + doc = bsoncore.AppendObjectIDElement(doc, "ao", oid) + doc = bsoncore.AppendDocumentElement(doc, "ap", buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar"))) + doc = bsoncore.AppendArrayElement(doc, "aq", + buildArray(bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "foo"), "1", "bar")), + ) + doc = bsoncore.AppendDocumentElement(doc, "ar", + buildDocument(bsoncore.AppendDoubleElement(bsoncore.AppendStringElement(nil, "hello", "world"), "pi", 3.14159)), + ) + doc = bsoncore.AppendNullElement(doc, "as") + doc = bsoncore.AppendNullElement(doc, "at") + doc = bsoncore.AppendCodeWithScopeElement(doc, "au", + "var hello = 'world';", buildDocument(bsoncore.AppendDoubleElement(nil, "pi", 3.14159)), + ) + for _, name := range [5]string{"av", "aw", "ax", "ay", "az"} { + doc = bsoncore.AppendDocumentElement(doc, name, buildDocument( + bsoncore.AppendDocumentElement(nil, "foo", buildDocument( + bsoncore.AppendStringElement(nil, "bar", "baz"), + )), + )) + } + return doc + }(nil)), + nil, + }, + { + "struct{[]interface{}}", + struct { + A []bool + B []int32 + C []int64 + D []uint16 + E []uint64 + F []float64 + G []string + H []map[string]string + I [][]byte + K [1][2]string + L []struct { + M string + } + N [][]string + R []primitive.ObjectID + T []struct{} + W []map[string]struct{} + X []map[string]struct{} + Y []map[string]struct{} + Z []time.Time + AA []json.Number + AB []*url.URL + AC []primitive.Decimal128 + AD []*time.Time + AE []*testValueUnmarshaler + AF []*bool + AG []*int32 + AH []*int64 + AI []*primitive.ObjectID + AJ []primitive.D + AK []primitive.A + AL [][2]primitive.E + }{ + A: []bool{true}, + B: []int32{123}, + C: []int64{456}, + D: []uint16{789}, + E: []uint64{101112}, + F: []float64{3.14159}, + G: []string{"Hello, world"}, + H: []map[string]string{{"foo": "bar"}}, + I: [][]byte{{0x01, 0x02, 0x03}}, + K: [1][2]string{{"baz", "qux"}}, + L: []struct { + M string + }{ + { + M: "foobar", + }, + }, + N: [][]string{{"foo", "bar"}}, + R: oids, + T: nil, + W: nil, + X: []map[string]struct{}{}, // Should be empty BSON Array + Y: []map[string]struct{}{{}}, // Should be BSON array with one element, an empty BSON SubDocument + Z: []time.Time{now, now}, + AA: []json.Number{json.Number("5"), json.Number("10.1")}, + AB: []*url.URL{murl}, + AC: []primitive.Decimal128{decimal128}, + AD: []*time.Time{&now, &now}, + AE: []*testValueUnmarshaler{ + {t: bsontype.String, val: bsoncore.AppendString(nil, "hello")}, + {t: bsontype.String, val: bsoncore.AppendString(nil, "world")}, + }, + AF: []*bool{pbool(true), nil}, + AG: []*int32{pi32(12345), nil}, + AH: []*int64{pi64(1234567890), nil, pi64(9012345678)}, + AI: []*primitive.ObjectID{&oid, nil}, + AJ: []primitive.D{{{"foo", "bar"}}, nil}, + AK: []primitive.A{{"foo", "bar"}, nil}, + AL: [][2]primitive.E{{{"hello", "world"}, {"pi", 3.14159}}}, + }, + buildDocument(func(doc []byte) []byte { + doc = appendArrayElement(doc, "a", bsoncore.AppendBooleanElement(nil, "0", true)) + doc = appendArrayElement(doc, "b", bsoncore.AppendInt32Element(nil, "0", 123)) + doc = appendArrayElement(doc, "c", bsoncore.AppendInt64Element(nil, "0", 456)) + doc = appendArrayElement(doc, "d", bsoncore.AppendInt32Element(nil, "0", 789)) + doc = appendArrayElement(doc, "e", bsoncore.AppendInt64Element(nil, "0", 101112)) + doc = appendArrayElement(doc, "f", bsoncore.AppendDoubleElement(nil, "0", 3.14159)) + doc = appendArrayElement(doc, "g", bsoncore.AppendStringElement(nil, "0", "Hello, world")) + doc = appendArrayElement(doc, "h", bsoncore.BuildDocumentElement(nil, "0", bsoncore.AppendStringElement(nil, "foo", "bar"))) + doc = appendArrayElement(doc, "i", bsoncore.AppendBinaryElement(nil, "0", 0x00, []byte{0x01, 0x02, 0x03})) + doc = appendArrayElement(doc, "k", + appendArrayElement(nil, "0", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "baz"), "1", "qux")), + ) + doc = appendArrayElement(doc, "l", bsoncore.BuildDocumentElement(nil, "0", bsoncore.AppendStringElement(nil, "m", "foobar"))) + doc = appendArrayElement(doc, "n", + appendArrayElement(nil, "0", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "foo"), "1", "bar")), + ) + doc = appendArrayElement(doc, "r", + bsoncore.AppendObjectIDElement( + bsoncore.AppendObjectIDElement( + bsoncore.AppendObjectIDElement(nil, + "0", oids[0]), + "1", oids[1]), + "2", oids[2]), + ) + doc = bsoncore.AppendNullElement(doc, "t") + doc = bsoncore.AppendNullElement(doc, "w") + doc = appendArrayElement(doc, "x", nil) + doc = appendArrayElement(doc, "y", bsoncore.BuildDocumentElement(nil, "0", nil)) + doc = appendArrayElement(doc, "z", + bsoncore.AppendDateTimeElement( + bsoncore.AppendDateTimeElement( + nil, "0", now.UnixNano()/int64(time.Millisecond)), + "1", now.UnixNano()/int64(time.Millisecond)), + ) + doc = appendArrayElement(doc, "aa", bsoncore.AppendDoubleElement(bsoncore.AppendInt64Element(nil, "0", 5), "1", 10.10)) + doc = appendArrayElement(doc, "ab", bsoncore.AppendStringElement(nil, "0", murl.String())) + doc = appendArrayElement(doc, "ac", bsoncore.AppendDecimal128Element(nil, "0", decimal128)) + doc = appendArrayElement(doc, "ad", + bsoncore.AppendDateTimeElement( + bsoncore.AppendDateTimeElement(nil, "0", now.UnixNano()/int64(time.Millisecond)), + "1", now.UnixNano()/int64(time.Millisecond)), + ) + doc = appendArrayElement(doc, "ae", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "hello"), "1", "world"), + ) + doc = appendArrayElement(doc, "af", + bsoncore.AppendNullElement(bsoncore.AppendBooleanElement(nil, "0", true), "1"), + ) + doc = appendArrayElement(doc, "ag", + bsoncore.AppendNullElement(bsoncore.AppendInt32Element(nil, "0", 12345), "1"), + ) + doc = appendArrayElement(doc, "ah", + bsoncore.AppendInt64Element( + bsoncore.AppendNullElement(bsoncore.AppendInt64Element(nil, "0", 1234567890), "1"), + "2", 9012345678, + ), + ) + doc = appendArrayElement(doc, "ai", + bsoncore.AppendNullElement(bsoncore.AppendObjectIDElement(nil, "0", oid), "1"), + ) + doc = appendArrayElement(doc, "aj", + bsoncore.AppendNullElement( + bsoncore.AppendDocumentElement(nil, "0", buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar"))), + "1", + ), + ) + doc = appendArrayElement(doc, "ak", + bsoncore.AppendNullElement( + appendArrayElement(nil, "0", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "foo"), "1", "bar"), + ), + "1", + ), + ) + doc = appendArrayElement(doc, "al", + bsoncore.BuildDocumentElement(nil, "0", + bsoncore.AppendDoubleElement(bsoncore.AppendStringElement(nil, "hello", "world"), "pi", 3.14159), + ), + ) + return doc + }(nil)), + nil, + }, + } + + t.Run("Decode", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + vr := bsonrw.NewBSONDocumentReader(tc.b) + reg := buildDefaultRegistry() + vtype := reflect.TypeOf(tc.value) + dec, err := reg.LookupDecoder(vtype) + noerr(t, err) + + gotVal := reflect.New(reflect.TypeOf(tc.value)).Elem() + err = dec.DecodeValue(DecodeContext{Registry: reg}, vr, gotVal) + noerr(t, err) + + got := gotVal.Interface() + want := tc.value + if diff := cmp.Diff( + got, want, + cmp.Comparer(compareDecimal128), + cmp.Comparer(compareNoPrivateFields), + cmp.Comparer(compareZeroTest), + cmp.Comparer(compareTime), + ); diff != "" { + t.Errorf("difference:\n%s", diff) + t.Errorf("Values are not equal.\ngot: %#v\nwant:%#v", got, want) + } + }) + } + }) + }) + t.Run("error path", func(t *testing.T) { + testCases := []struct { + name string + value interface{} + b []byte + err error + }{ + { + "duplicate name struct", + struct { + A int64 + B int64 `bson:"a"` + }{ + A: 0, + B: 54321, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + fmt.Errorf("duplicated key a"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + vr := bsonrw.NewBSONDocumentReader(tc.b) + reg := buildDefaultRegistry() + vtype := reflect.TypeOf(tc.value) + dec, err := reg.LookupDecoder(vtype) + noerr(t, err) + + gotVal := reflect.New(reflect.TypeOf(tc.value)).Elem() + err = dec.DecodeValue(DecodeContext{Registry: reg}, vr, gotVal) + if err == nil || !strings.Contains(err.Error(), tc.err.Error()) { + t.Errorf("Did not receive expected error. got %v; want %v", err, tc.err) + } + }) + } + }) + + t.Run("defaultEmptyInterfaceCodec.DecodeValue", func(t *testing.T) { + t.Run("DecodeValue", func(t *testing.T) { + testCases := []struct { + name string + val interface{} + bsontype bsontype.Type + }{ + { + "Double - float64", + float64(3.14159), + bsontype.Double, + }, + { + "String - string", + "foo bar baz", + bsontype.String, + }, + { + "Array - primitive.A", + primitive.A{3.14159}, + bsontype.Array, + }, + { + "Binary - Binary", + primitive.Binary{Subtype: 0xFF, Data: []byte{0x01, 0x02, 0x03}}, + bsontype.Binary, + }, + { + "Undefined - Undefined", + primitive.Undefined{}, + bsontype.Undefined, + }, + { + "ObjectID - primitive.ObjectID", + primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + bsontype.ObjectID, + }, + { + "Boolean - bool", + bool(true), + bsontype.Boolean, + }, + { + "DateTime - DateTime", + primitive.DateTime(1234567890), + bsontype.DateTime, + }, + { + "Null - Null", + nil, + bsontype.Null, + }, + { + "Regex - Regex", + primitive.Regex{Pattern: "foo", Options: "bar"}, + bsontype.Regex, + }, + { + "DBPointer - DBPointer", + primitive.DBPointer{ + DB: "foobar", + Pointer: primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + }, + bsontype.DBPointer, + }, + { + "JavaScript - JavaScript", + primitive.JavaScript("var foo = 'bar';"), + bsontype.JavaScript, + }, + { + "Symbol - Symbol", + primitive.Symbol("foobarbazlolz"), + bsontype.Symbol, + }, + { + "Int32 - int32", + int32(123456), + bsontype.Int32, + }, + { + "Int64 - int64", + int64(1234567890), + bsontype.Int64, + }, + { + "Timestamp - Timestamp", + primitive.Timestamp{T: 12345, I: 67890}, + bsontype.Timestamp, + }, + { + "Decimal128 - decimal.Decimal128", + primitive.NewDecimal128(12345, 67890), + bsontype.Decimal128, + }, + { + "MinKey - MinKey", + primitive.MinKey{}, + bsontype.MinKey, + }, + { + "MaxKey - MaxKey", + primitive.MaxKey{}, + bsontype.MaxKey, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + llvr := &bsonrwtest.ValueReaderWriter{BSONType: tc.bsontype} + + t.Run("Type Map failure", func(t *testing.T) { + if tc.bsontype == bsontype.Null { + t.Skip() + } + val := reflect.New(tEmpty).Elem() + dc := DecodeContext{Registry: NewRegistryBuilder().Build()} + want := ErrNoTypeMapEntry{Type: tc.bsontype} + got := defaultEmptyInterfaceCodec.DecodeValue(dc, llvr, val) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + + t.Run("Lookup failure", func(t *testing.T) { + if tc.bsontype == bsontype.Null { + t.Skip() + } + val := reflect.New(tEmpty).Elem() + dc := DecodeContext{ + Registry: NewRegistryBuilder(). + RegisterTypeMapEntry(tc.bsontype, reflect.TypeOf(tc.val)). + Build(), + } + want := ErrNoDecoder{Type: reflect.TypeOf(tc.val)} + got := defaultEmptyInterfaceCodec.DecodeValue(dc, llvr, val) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + + t.Run("DecodeValue failure", func(t *testing.T) { + if tc.bsontype == bsontype.Null { + t.Skip() + } + want := errors.New("DecodeValue failure error") + llc := &llCodec{t: t, err: want} + dc := DecodeContext{ + Registry: NewRegistryBuilder(). + RegisterTypeDecoder(reflect.TypeOf(tc.val), llc). + RegisterTypeMapEntry(tc.bsontype, reflect.TypeOf(tc.val)). + Build(), + } + got := defaultEmptyInterfaceCodec.DecodeValue(dc, llvr, reflect.New(tEmpty).Elem()) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + + t.Run("Success", func(t *testing.T) { + want := tc.val + llc := &llCodec{t: t, decodeval: tc.val} + dc := DecodeContext{ + Registry: NewRegistryBuilder(). + RegisterTypeDecoder(reflect.TypeOf(tc.val), llc). + RegisterTypeMapEntry(tc.bsontype, reflect.TypeOf(tc.val)). + Build(), + } + got := reflect.New(tEmpty).Elem() + err := defaultEmptyInterfaceCodec.DecodeValue(dc, llvr, got) + noerr(t, err) + if !cmp.Equal(got.Interface(), want, cmp.Comparer(compareDecimal128)) { + t.Errorf("Did not receive expected value. got %v; want %v", got.Interface(), want) + } + }) + }) + } + }) + + t.Run("non-interface{}", func(t *testing.T) { + val := uint64(1234567890) + want := ValueDecoderError{Name: "EmptyInterfaceDecodeValue", Types: []reflect.Type{tEmpty}, Received: reflect.ValueOf(val)} + got := defaultEmptyInterfaceCodec.DecodeValue(DecodeContext{}, nil, reflect.ValueOf(val)) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + + t.Run("nil *interface{}", func(t *testing.T) { + var val interface{} + want := ValueDecoderError{Name: "EmptyInterfaceDecodeValue", Types: []reflect.Type{tEmpty}, Received: reflect.ValueOf(val)} + got := defaultEmptyInterfaceCodec.DecodeValue(DecodeContext{}, nil, reflect.ValueOf(val)) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + + t.Run("no type registered", func(t *testing.T) { + llvr := &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Double} + want := ErrNoTypeMapEntry{Type: bsontype.Double} + val := reflect.New(tEmpty).Elem() + got := defaultEmptyInterfaceCodec.DecodeValue(DecodeContext{Registry: NewRegistryBuilder().Build()}, llvr, val) + if !compareErrors(got, want) { + t.Errorf("Errors are not equal. got %v; want %v", got, want) + } + }) + t.Run("top level document", func(t *testing.T) { + data := bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159)) + vr := bsonrw.NewBSONDocumentReader(data) + want := primitive.D{{"pi", 3.14159}} + var got interface{} + val := reflect.ValueOf(&got).Elem() + err := defaultEmptyInterfaceCodec.DecodeValue(DecodeContext{Registry: buildDefaultRegistry()}, vr, val) + noerr(t, err) + if !cmp.Equal(got, want) { + t.Errorf("Did not get correct result. got %v; want %v", got, want) + } + }) + t.Run("custom type map entry", func(t *testing.T) { + // registering a custom type map entry for both bsontype.Type(0) anad bsontype.EmbeddedDocument should cause + // both top-level and embedded documents to decode to registered type when unmarshalling to interface{} + + topLevelRb := NewRegistryBuilder() + defaultValueEncoders.RegisterDefaultEncoders(topLevelRb) + defaultValueDecoders.RegisterDefaultDecoders(topLevelRb) + topLevelRb.RegisterTypeMapEntry(bsontype.Type(0), reflect.TypeOf(primitive.M{})) + + embeddedRb := NewRegistryBuilder() + defaultValueEncoders.RegisterDefaultEncoders(embeddedRb) + defaultValueDecoders.RegisterDefaultDecoders(embeddedRb) + embeddedRb.RegisterTypeMapEntry(bsontype.Type(0), reflect.TypeOf(primitive.M{})) + + // create doc {"nested": {"foo": 1}} + innerDoc := bsoncore.BuildDocument( + nil, + bsoncore.AppendInt32Element(nil, "foo", 1), + ) + doc := bsoncore.BuildDocument( + nil, + bsoncore.AppendDocumentElement(nil, "nested", innerDoc), + ) + want := primitive.M{ + "nested": primitive.M{ + "foo": int32(1), + }, + } + + testCases := []struct { + name string + registry *Registry + }{ + {"top level", topLevelRb.Build()}, + {"embedded", embeddedRb.Build()}, + } + for _, tc := range testCases { + var got interface{} + vr := bsonrw.NewBSONDocumentReader(doc) + val := reflect.ValueOf(&got).Elem() + + err := defaultEmptyInterfaceCodec.DecodeValue(DecodeContext{Registry: tc.registry}, vr, val) + noerr(t, err) + if !cmp.Equal(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + } + }) + t.Run("ancestor info is used over custom type map entry", func(t *testing.T) { + // If a type map entry is registered for bsontype.EmbeddedDocument, the decoder should use ancestor + // information if available instead of the registered entry. + + rb := NewRegistryBuilder() + defaultValueEncoders.RegisterDefaultEncoders(rb) + defaultValueDecoders.RegisterDefaultDecoders(rb) + rb.RegisterTypeMapEntry(bsontype.EmbeddedDocument, reflect.TypeOf(primitive.M{})) + reg := rb.Build() + + // build document {"nested": {"foo": 10}} + inner := bsoncore.BuildDocument( + nil, + bsoncore.AppendInt32Element(nil, "foo", 10), + ) + doc := bsoncore.BuildDocument( + nil, + bsoncore.AppendDocumentElement(nil, "nested", inner), + ) + want := primitive.D{ + {"nested", primitive.D{ + {"foo", int32(10)}, + }}, + } + + var got primitive.D + vr := bsonrw.NewBSONDocumentReader(doc) + val := reflect.ValueOf(&got).Elem() + err := defaultSliceCodec.DecodeValue(DecodeContext{Registry: reg}, vr, val) + noerr(t, err) + if !cmp.Equal(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + }) + }) + + t.Run("decode errors contain key information", func(t *testing.T) { + decodeValueError := errors.New("decode value error") + emptyInterfaceErrorDecode := func(DecodeContext, bsonrw.ValueReader, reflect.Value) error { + return decodeValueError + } + emptyInterfaceErrorRegistry := NewRegistryBuilder(). + RegisterTypeDecoder(tEmpty, ValueDecoderFunc(emptyInterfaceErrorDecode)).Build() + + // Set up a document {foo: 10} and an error that would happen if the value were decoded into interface{} + // using the registry defined above. + docBytes := bsoncore.BuildDocumentFromElements( + nil, + bsoncore.AppendInt32Element(nil, "foo", 10), + ) + docEmptyInterfaceErr := &DecodeError{ + keys: []string{"foo"}, + wrapped: decodeValueError, + } + + // Set up struct definitions where Foo maps to interface{} and string. When decoded using the registry defined + // above, the interface{} struct will get an error when calling DecodeValue and the string struct will get an + // error when looking up a decoder. + type emptyInterfaceStruct struct { + Foo interface{} + } + type stringStruct struct { + Foo string + } + emptyInterfaceStructErr := &DecodeError{ + keys: []string{"foo"}, + wrapped: decodeValueError, + } + stringStructErr := &DecodeError{ + keys: []string{"foo"}, + wrapped: ErrNoDecoder{reflect.TypeOf("")}, + } + + // Test a deeply nested struct mixed with maps and slices. + // Build document {"first": {"second": {"randomKey": {"third": [{}, {"fourth": "value"}]}}}} + type inner3 struct{ Fourth interface{} } + type inner2 struct{ Third []inner3 } + type inner1 struct{ Second map[string]inner2 } + type outer struct{ First inner1 } + inner3EmptyDoc := buildDocument(nil) + inner3Doc := buildDocument(bsoncore.AppendStringElement(nil, "fourth", "value")) + inner3Array := buildArray( + // buildArray takes []byte so we first append() all of the values into a single []byte + append( + bsoncore.AppendDocumentElement(nil, "0", inner3EmptyDoc), + bsoncore.AppendDocumentElement(nil, "1", inner3Doc)..., + ), + ) + inner2Doc := buildDocument(bsoncore.AppendArrayElement(nil, "third", inner3Array)) + inner2Map := buildDocument(bsoncore.AppendDocumentElement(nil, "randomKey", inner2Doc)) + inner1Doc := buildDocument(bsoncore.AppendDocumentElement(nil, "second", inner2Map)) + outerDoc := buildDocument(bsoncore.AppendDocumentElement(nil, "first", inner1Doc)) + + // Use a registry that has all default decoders with the custom interface{} decoder that always errors. + nestedRegistryBuilder := NewRegistryBuilder() + defaultValueDecoders.RegisterDefaultDecoders(nestedRegistryBuilder) + nestedRegistry := nestedRegistryBuilder. + RegisterTypeDecoder(tEmpty, ValueDecoderFunc(emptyInterfaceErrorDecode)). + Build() + nestedErr := &DecodeError{ + keys: []string{"fourth", "1", "third", "randomKey", "second", "first"}, + wrapped: decodeValueError, + } + + testCases := []struct { + name string + val interface{} + vr bsonrw.ValueReader + registry *Registry // buildDefaultRegistry will be used if this is nil + decoder ValueDecoder + err error + }{ + { + // DecodeValue error when decoding into a primitive.D. + "primitive.D slice", + primitive.D{}, + bsonrw.NewBSONDocumentReader(docBytes), + emptyInterfaceErrorRegistry, + defaultSliceCodec, + docEmptyInterfaceErr, + }, + { + // DecodeValue error when decoding into a []string. + "string slice", + []string{}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + nil, + defaultSliceCodec, + &DecodeError{ + keys: []string{"0"}, + wrapped: errors.New("cannot decode array into a string type"), + }, + }, + { + // DecodeValue error when decoding into a primitive.E array. This should have the same behavior as + // the "primitive.D slice" test above because both the defaultSliceCodec and ArrayDecodeValue use + // the decodeD helper function. + "primitive.D array", + [1]primitive.E{}, + bsonrw.NewBSONDocumentReader(docBytes), + emptyInterfaceErrorRegistry, + ValueDecoderFunc(dvd.ArrayDecodeValue), + docEmptyInterfaceErr, + }, + { + // DecodeValue error when decoding into a string array. This should have the same behavior as + // the "primitive.D slice" test above because both the defaultSliceCodec and ArrayDecodeValue use + // the decodeDefault helper function. + "string array", + [1]string{}, + &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Array}, + nil, + ValueDecoderFunc(dvd.ArrayDecodeValue), + &DecodeError{ + keys: []string{"0"}, + wrapped: errors.New("cannot decode array into a string type"), + }, + }, + { + // DecodeValue error when decoding into a map. + "map", + map[string]interface{}{}, + bsonrw.NewBSONDocumentReader(docBytes), + emptyInterfaceErrorRegistry, + defaultMapCodec, + docEmptyInterfaceErr, + }, + { + // DecodeValue error when decoding into a struct. + "struct - DecodeValue error", + emptyInterfaceStruct{}, + bsonrw.NewBSONDocumentReader(docBytes), + emptyInterfaceErrorRegistry, + defaultTestStructCodec, + emptyInterfaceStructErr, + }, + { + // ErrNoDecoder when decoding into a struct. + // This test uses NewRegistryBuilder().Build rather than buildDefaultRegistry to ensure that there is + // no decoder for strings. + "struct - no decoder found", + stringStruct{}, + bsonrw.NewBSONDocumentReader(docBytes), + NewRegistryBuilder().Build(), + defaultTestStructCodec, + stringStructErr, + }, + { + "deeply nested struct", + outer{}, + bsonrw.NewBSONDocumentReader(outerDoc), + nestedRegistry, + defaultTestStructCodec, + nestedErr, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dc := DecodeContext{Registry: tc.registry} + if dc.Registry == nil { + dc.Registry = buildDefaultRegistry() + } + + var val reflect.Value + if rtype := reflect.TypeOf(tc.val); rtype != nil { + val = reflect.New(rtype).Elem() + } + err := tc.decoder.DecodeValue(dc, tc.vr, val) + assert.Equal(t, tc.err, err, "expected error %v, got %v", tc.err, err) + }) + } + + t.Run("keys are correctly reversed", func(t *testing.T) { + innerBytes := bsoncore.BuildDocumentFromElements(nil, bsoncore.AppendInt32Element(nil, "bar", 10)) + outerBytes := bsoncore.BuildDocumentFromElements(nil, bsoncore.AppendDocumentElement(nil, "foo", innerBytes)) + + type inner struct{ Bar string } + type outer struct{ Foo inner } + + dc := DecodeContext{Registry: buildDefaultRegistry()} + vr := bsonrw.NewBSONDocumentReader(outerBytes) + val := reflect.New(reflect.TypeOf(outer{})).Elem() + err := defaultTestStructCodec.DecodeValue(dc, vr, val) + + decodeErr, ok := err.(*DecodeError) + assert.True(t, ok, "expected DecodeError, got %v of type %T", err, err) + expectedKeys := []string{"foo", "bar"} + assert.Equal(t, expectedKeys, decodeErr.Keys(), "expected keys slice %v, got %v", expectedKeys, + decodeErr.Keys()) + keyPath := strings.Join(expectedKeys, ".") + assert.True(t, strings.Contains(decodeErr.Error(), keyPath), + "expected error %v to contain key pattern %s", decodeErr, keyPath) + }) + }) + + t.Run("values are converted", func(t *testing.T) { + // When decoding into a D or M, values must be converted if they are not being decoded to the default type. + + t.Run("D", func(t *testing.T) { + trueValue := bsoncore.Value{ + Type: bsontype.Boolean, + Data: bsoncore.AppendBoolean(nil, true), + } + docBytes := bsoncore.BuildDocumentFromElements(nil, + bsoncore.AppendBooleanElement(nil, "bool", true), + bsoncore.BuildArrayElement(nil, "boolArray", trueValue), + ) + + rb := NewRegistryBuilder() + defaultValueDecoders.RegisterDefaultDecoders(rb) + reg := rb.RegisterTypeMapEntry(bsontype.Boolean, reflect.TypeOf(mybool(true))).Build() + + dc := DecodeContext{Registry: reg} + vr := bsonrw.NewBSONDocumentReader(docBytes) + val := reflect.New(tD).Elem() + err := defaultValueDecoders.DDecodeValue(dc, vr, val) + assert.Nil(t, err, "DDecodeValue error: %v", err) + + want := primitive.D{ + {"bool", mybool(true)}, + {"boolArray", primitive.A{mybool(true)}}, + } + got := val.Interface().(primitive.D) + assert.Equal(t, want, got, "want document %v, got %v", want, got) + }) + t.Run("M", func(t *testing.T) { + docBytes := bsoncore.BuildDocumentFromElements(nil, + bsoncore.AppendBooleanElement(nil, "bool", true), + ) + + type myMap map[string]mybool + dc := DecodeContext{Registry: buildDefaultRegistry()} + vr := bsonrw.NewBSONDocumentReader(docBytes) + val := reflect.New(reflect.TypeOf(myMap{})).Elem() + err := defaultMapCodec.DecodeValue(dc, vr, val) + assert.Nil(t, err, "DecodeValue error: %v", err) + + want := myMap{ + "bool": mybool(true), + } + got := val.Interface().(myMap) + assert.Equal(t, want, got, "expected map %v, got %v", want, got) + }) + }) +} + +type testValueUnmarshaler struct { + t bsontype.Type + val []byte + err error +} + +func (tvu *testValueUnmarshaler) UnmarshalBSONValue(t bsontype.Type, val []byte) error { + tvu.t, tvu.val = t, val + return tvu.err +} + +type testUnmarshaler struct { + Val []byte + Err error +} + +func (tvu *testUnmarshaler) UnmarshalBSON(val []byte) error { + tvu.Val = val + return tvu.Err +} + +func (tvu testValueUnmarshaler) Equal(tvu2 testValueUnmarshaler) bool { + return tvu.t == tvu2.t && bytes.Equal(tvu.val, tvu2.val) +} + +// buildDocumentArray inserts vals inside of an array inside of a document. +func buildDocumentArray(fn func([]byte) []byte) []byte { + aix, doc := bsoncore.AppendArrayElementStart(nil, "Z") + doc = fn(doc) + doc, _ = bsoncore.AppendArrayEnd(doc, aix) + return buildDocument(doc) +} + +func buildArray(vals []byte) []byte { + aix, doc := bsoncore.AppendArrayStart(nil) + doc = append(doc, vals...) + doc, _ = bsoncore.AppendArrayEnd(doc, aix) + return doc +} + +func appendArrayElement(dst []byte, key string, vals []byte) []byte { + aix, doc := bsoncore.AppendArrayElementStart(dst, key) + doc = append(doc, vals...) + doc, _ = bsoncore.AppendArrayEnd(doc, aix) + return doc +} + +// buildDocument inserts elems inside of a document. +func buildDocument(elems []byte) []byte { + idx, doc := bsoncore.AppendDocumentStart(nil) + doc = append(doc, elems...) + doc, _ = bsoncore.AppendDocumentEnd(doc, idx) + return doc +} diff --git a/mongo/bson/bsoncodec/default_value_encoders.go b/mongo/bson/bsoncodec/default_value_encoders.go new file mode 100644 index 0000000..6bdb43c --- /dev/null +++ b/mongo/bson/bsoncodec/default_value_encoders.go @@ -0,0 +1,766 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "net/url" + "reflect" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +var defaultValueEncoders DefaultValueEncoders + +var bvwPool = bsonrw.NewBSONValueWriterPool() + +var errInvalidValue = errors.New("cannot encode invalid element") + +var sliceWriterPool = sync.Pool{ + New: func() interface{} { + sw := make(bsonrw.SliceWriter, 0) + return &sw + }, +} + +func encodeElement(ec EncodeContext, dw bsonrw.DocumentWriter, e primitive.E) error { + vw, err := dw.WriteDocumentElement(e.Key) + if err != nil { + return err + } + + if e.Value == nil { + return vw.WriteNull() + } + encoder, err := ec.LookupEncoder(reflect.TypeOf(e.Value)) + if err != nil { + return err + } + + err = encoder.EncodeValue(ec, vw, reflect.ValueOf(e.Value)) + if err != nil { + return err + } + return nil +} + +// DefaultValueEncoders is a namespace type for the default ValueEncoders used +// when creating a registry. +type DefaultValueEncoders struct{} + +// RegisterDefaultEncoders will register the encoder methods attached to DefaultValueEncoders with +// the provided RegistryBuilder. +func (dve DefaultValueEncoders) RegisterDefaultEncoders(rb *RegistryBuilder) { + if rb == nil { + panic(errors.New("argument to RegisterDefaultEncoders must not be nil")) + } + rb. + RegisterTypeEncoder(tByteSlice, defaultByteSliceCodec). + RegisterTypeEncoder(tTime, defaultTimeCodec). + RegisterTypeEncoder(tEmpty, defaultEmptyInterfaceCodec). + RegisterTypeEncoder(tCoreArray, defaultArrayCodec). + RegisterTypeEncoder(tOID, ValueEncoderFunc(dve.ObjectIDEncodeValue)). + RegisterTypeEncoder(tDecimal, ValueEncoderFunc(dve.Decimal128EncodeValue)). + RegisterTypeEncoder(tJSONNumber, ValueEncoderFunc(dve.JSONNumberEncodeValue)). + RegisterTypeEncoder(tURL, ValueEncoderFunc(dve.URLEncodeValue)). + RegisterTypeEncoder(tJavaScript, ValueEncoderFunc(dve.JavaScriptEncodeValue)). + RegisterTypeEncoder(tSymbol, ValueEncoderFunc(dve.SymbolEncodeValue)). + RegisterTypeEncoder(tBinary, ValueEncoderFunc(dve.BinaryEncodeValue)). + RegisterTypeEncoder(tUndefined, ValueEncoderFunc(dve.UndefinedEncodeValue)). + RegisterTypeEncoder(tDateTime, ValueEncoderFunc(dve.DateTimeEncodeValue)). + RegisterTypeEncoder(tNull, ValueEncoderFunc(dve.NullEncodeValue)). + RegisterTypeEncoder(tRegex, ValueEncoderFunc(dve.RegexEncodeValue)). + RegisterTypeEncoder(tDBPointer, ValueEncoderFunc(dve.DBPointerEncodeValue)). + RegisterTypeEncoder(tTimestamp, ValueEncoderFunc(dve.TimestampEncodeValue)). + RegisterTypeEncoder(tMinKey, ValueEncoderFunc(dve.MinKeyEncodeValue)). + RegisterTypeEncoder(tMaxKey, ValueEncoderFunc(dve.MaxKeyEncodeValue)). + RegisterTypeEncoder(tCoreDocument, ValueEncoderFunc(dve.CoreDocumentEncodeValue)). + RegisterTypeEncoder(tCodeWithScope, ValueEncoderFunc(dve.CodeWithScopeEncodeValue)). + RegisterDefaultEncoder(reflect.Bool, ValueEncoderFunc(dve.BooleanEncodeValue)). + RegisterDefaultEncoder(reflect.Int, ValueEncoderFunc(dve.IntEncodeValue)). + RegisterDefaultEncoder(reflect.Int8, ValueEncoderFunc(dve.IntEncodeValue)). + RegisterDefaultEncoder(reflect.Int16, ValueEncoderFunc(dve.IntEncodeValue)). + RegisterDefaultEncoder(reflect.Int32, ValueEncoderFunc(dve.IntEncodeValue)). + RegisterDefaultEncoder(reflect.Int64, ValueEncoderFunc(dve.IntEncodeValue)). + RegisterDefaultEncoder(reflect.Uint, defaultUIntCodec). + RegisterDefaultEncoder(reflect.Uint8, defaultUIntCodec). + RegisterDefaultEncoder(reflect.Uint16, defaultUIntCodec). + RegisterDefaultEncoder(reflect.Uint32, defaultUIntCodec). + RegisterDefaultEncoder(reflect.Uint64, defaultUIntCodec). + RegisterDefaultEncoder(reflect.Float32, ValueEncoderFunc(dve.FloatEncodeValue)). + RegisterDefaultEncoder(reflect.Float64, ValueEncoderFunc(dve.FloatEncodeValue)). + RegisterDefaultEncoder(reflect.Array, ValueEncoderFunc(dve.ArrayEncodeValue)). + RegisterDefaultEncoder(reflect.Map, defaultMapCodec). + RegisterDefaultEncoder(reflect.Slice, defaultSliceCodec). + RegisterDefaultEncoder(reflect.String, defaultStringCodec). + RegisterDefaultEncoder(reflect.Struct, newDefaultStructCodec()). + RegisterDefaultEncoder(reflect.Ptr, NewPointerCodec()). + RegisterHookEncoder(tValueMarshaler, ValueEncoderFunc(dve.ValueMarshalerEncodeValue)). + RegisterHookEncoder(tMarshaler, ValueEncoderFunc(dve.MarshalerEncodeValue)). + RegisterHookEncoder(tProxy, ValueEncoderFunc(dve.ProxyEncodeValue)) +} + +// BooleanEncodeValue is the ValueEncoderFunc for bool types. +func (dve DefaultValueEncoders) BooleanEncodeValue(ectx EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Bool { + return ValueEncoderError{Name: "BooleanEncodeValue", Kinds: []reflect.Kind{reflect.Bool}, Received: val} + } + return vw.WriteBoolean(val.Bool()) +} + +func fitsIn32Bits(i int64) bool { + return math.MinInt32 <= i && i <= math.MaxInt32 +} + +// IntEncodeValue is the ValueEncoderFunc for int types. +func (dve DefaultValueEncoders) IntEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + switch val.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32: + return vw.WriteInt32(int32(val.Int())) + case reflect.Int: + i64 := val.Int() + if fitsIn32Bits(i64) { + return vw.WriteInt32(int32(i64)) + } + return vw.WriteInt64(i64) + case reflect.Int64: + i64 := val.Int() + if ec.MinSize && fitsIn32Bits(i64) { + return vw.WriteInt32(int32(i64)) + } + return vw.WriteInt64(i64) + } + + return ValueEncoderError{ + Name: "IntEncodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: val, + } +} + +// UintEncodeValue is the ValueEncoderFunc for uint types. +// +// Deprecated: UintEncodeValue is not registered by default. Use UintCodec.EncodeValue instead. +func (dve DefaultValueEncoders) UintEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + switch val.Kind() { + case reflect.Uint8, reflect.Uint16: + return vw.WriteInt32(int32(val.Uint())) + case reflect.Uint, reflect.Uint32, reflect.Uint64: + u64 := val.Uint() + if ec.MinSize && u64 <= math.MaxInt32 { + return vw.WriteInt32(int32(u64)) + } + if u64 > math.MaxInt64 { + return fmt.Errorf("%d overflows int64", u64) + } + return vw.WriteInt64(int64(u64)) + } + + return ValueEncoderError{ + Name: "UintEncodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: val, + } +} + +// FloatEncodeValue is the ValueEncoderFunc for float types. +func (dve DefaultValueEncoders) FloatEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + switch val.Kind() { + case reflect.Float32, reflect.Float64: + return vw.WriteDouble(val.Float()) + } + + return ValueEncoderError{Name: "FloatEncodeValue", Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, Received: val} +} + +// StringEncodeValue is the ValueEncoderFunc for string types. +// +// Deprecated: StringEncodeValue is not registered by default. Use StringCodec.EncodeValue instead. +func (dve DefaultValueEncoders) StringEncodeValue(ectx EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if val.Kind() != reflect.String { + return ValueEncoderError{ + Name: "StringEncodeValue", + Kinds: []reflect.Kind{reflect.String}, + Received: val, + } + } + + return vw.WriteString(val.String()) +} + +// ObjectIDEncodeValue is the ValueEncoderFunc for primitive.ObjectID. +func (dve DefaultValueEncoders) ObjectIDEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tOID { + return ValueEncoderError{Name: "ObjectIDEncodeValue", Types: []reflect.Type{tOID}, Received: val} + } + return vw.WriteObjectID(val.Interface().(primitive.ObjectID)) +} + +// Decimal128EncodeValue is the ValueEncoderFunc for primitive.Decimal128. +func (dve DefaultValueEncoders) Decimal128EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tDecimal { + return ValueEncoderError{Name: "Decimal128EncodeValue", Types: []reflect.Type{tDecimal}, Received: val} + } + return vw.WriteDecimal128(val.Interface().(primitive.Decimal128)) +} + +// JSONNumberEncodeValue is the ValueEncoderFunc for json.Number. +func (dve DefaultValueEncoders) JSONNumberEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tJSONNumber { + return ValueEncoderError{Name: "JSONNumberEncodeValue", Types: []reflect.Type{tJSONNumber}, Received: val} + } + jsnum := val.Interface().(json.Number) + + // Attempt int first, then float64 + if i64, err := jsnum.Int64(); err == nil { + return dve.IntEncodeValue(ec, vw, reflect.ValueOf(i64)) + } + + f64, err := jsnum.Float64() + if err != nil { + return err + } + + return dve.FloatEncodeValue(ec, vw, reflect.ValueOf(f64)) +} + +// URLEncodeValue is the ValueEncoderFunc for url.URL. +func (dve DefaultValueEncoders) URLEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tURL { + return ValueEncoderError{Name: "URLEncodeValue", Types: []reflect.Type{tURL}, Received: val} + } + u := val.Interface().(url.URL) + return vw.WriteString(u.String()) +} + +// TimeEncodeValue is the ValueEncoderFunc for time.TIme. +// +// Deprecated: TimeEncodeValue is not registered by default. Use TimeCodec.EncodeValue instead. +func (dve DefaultValueEncoders) TimeEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tTime { + return ValueEncoderError{Name: "TimeEncodeValue", Types: []reflect.Type{tTime}, Received: val} + } + tt := val.Interface().(time.Time) + dt := primitive.NewDateTimeFromTime(tt) + return vw.WriteDateTime(int64(dt)) +} + +// ByteSliceEncodeValue is the ValueEncoderFunc for []byte. +// +// Deprecated: ByteSliceEncodeValue is not registered by default. Use ByteSliceCodec.EncodeValue instead. +func (dve DefaultValueEncoders) ByteSliceEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tByteSlice { + return ValueEncoderError{Name: "ByteSliceEncodeValue", Types: []reflect.Type{tByteSlice}, Received: val} + } + if val.IsNil() { + return vw.WriteNull() + } + return vw.WriteBinary(val.Interface().([]byte)) +} + +// MapEncodeValue is the ValueEncoderFunc for map[string]* types. +// +// Deprecated: MapEncodeValue is not registered by default. Use MapCodec.EncodeValue instead. +func (dve DefaultValueEncoders) MapEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Map || val.Type().Key().Kind() != reflect.String { + return ValueEncoderError{Name: "MapEncodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: val} + } + + if val.IsNil() { + // If we have a nill map but we can't WriteNull, that means we're probably trying to encode + // to a TopLevel document. We can't currently tell if this is what actually happened, but if + // there's a deeper underlying problem, the error will also be returned from WriteDocument, + // so just continue. The operations on a map reflection value are valid, so we can call + // MapKeys within mapEncodeValue without a problem. + err := vw.WriteNull() + if err == nil { + return nil + } + } + + dw, err := vw.WriteDocument() + if err != nil { + return err + } + + return dve.mapEncodeValue(ec, dw, val, nil) +} + +// mapEncodeValue handles encoding of the values of a map. The collisionFn returns +// true if the provided key exists, this is mainly used for inline maps in the +// struct codec. +func (dve DefaultValueEncoders) mapEncodeValue(ec EncodeContext, dw bsonrw.DocumentWriter, val reflect.Value, collisionFn func(string) bool) error { + + elemType := val.Type().Elem() + encoder, err := ec.LookupEncoder(elemType) + if err != nil && elemType.Kind() != reflect.Interface { + return err + } + + keys := val.MapKeys() + for _, key := range keys { + if collisionFn != nil && collisionFn(key.String()) { + return fmt.Errorf("Key %s of inlined map conflicts with a struct field name", key) + } + + currEncoder, currVal, lookupErr := dve.lookupElementEncoder(ec, encoder, val.MapIndex(key)) + if lookupErr != nil && lookupErr != errInvalidValue { + return lookupErr + } + + vw, err := dw.WriteDocumentElement(key.String()) + if err != nil { + return err + } + + if lookupErr == errInvalidValue { + err = vw.WriteNull() + if err != nil { + return err + } + continue + } + + err = currEncoder.EncodeValue(ec, vw, currVal) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() +} + +// ArrayEncodeValue is the ValueEncoderFunc for array types. +func (dve DefaultValueEncoders) ArrayEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Array { + return ValueEncoderError{Name: "ArrayEncodeValue", Kinds: []reflect.Kind{reflect.Array}, Received: val} + } + + // If we have a []primitive.E we want to treat it as a document instead of as an array. + if val.Type().Elem() == tE { + dw, err := vw.WriteDocument() + if err != nil { + return err + } + + for idx := 0; idx < val.Len(); idx++ { + e := val.Index(idx).Interface().(primitive.E) + err = encodeElement(ec, dw, e) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() + } + + // If we have a []byte we want to treat it as a binary instead of as an array. + if val.Type().Elem() == tByte { + var byteSlice []byte + for idx := 0; idx < val.Len(); idx++ { + byteSlice = append(byteSlice, val.Index(idx).Interface().(byte)) + } + return vw.WriteBinary(byteSlice) + } + + aw, err := vw.WriteArray() + if err != nil { + return err + } + + elemType := val.Type().Elem() + encoder, err := ec.LookupEncoder(elemType) + if err != nil && elemType.Kind() != reflect.Interface { + return err + } + + for idx := 0; idx < val.Len(); idx++ { + currEncoder, currVal, lookupErr := dve.lookupElementEncoder(ec, encoder, val.Index(idx)) + if lookupErr != nil && lookupErr != errInvalidValue { + return lookupErr + } + + vw, err := aw.WriteArrayElement() + if err != nil { + return err + } + + if lookupErr == errInvalidValue { + err = vw.WriteNull() + if err != nil { + return err + } + continue + } + + err = currEncoder.EncodeValue(ec, vw, currVal) + if err != nil { + return err + } + } + return aw.WriteArrayEnd() +} + +// SliceEncodeValue is the ValueEncoderFunc for slice types. +// +// Deprecated: SliceEncodeValue is not registered by default. Use SliceCodec.EncodeValue instead. +func (dve DefaultValueEncoders) SliceEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Slice { + return ValueEncoderError{Name: "SliceEncodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: val} + } + + if val.IsNil() { + return vw.WriteNull() + } + + // If we have a []primitive.E we want to treat it as a document instead of as an array. + if val.Type().ConvertibleTo(tD) { + d := val.Convert(tD).Interface().(primitive.D) + + dw, err := vw.WriteDocument() + if err != nil { + return err + } + + for _, e := range d { + err = encodeElement(ec, dw, e) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() + } + + aw, err := vw.WriteArray() + if err != nil { + return err + } + + elemType := val.Type().Elem() + encoder, err := ec.LookupEncoder(elemType) + if err != nil && elemType.Kind() != reflect.Interface { + return err + } + + for idx := 0; idx < val.Len(); idx++ { + currEncoder, currVal, lookupErr := dve.lookupElementEncoder(ec, encoder, val.Index(idx)) + if lookupErr != nil && lookupErr != errInvalidValue { + return lookupErr + } + + vw, err := aw.WriteArrayElement() + if err != nil { + return err + } + + if lookupErr == errInvalidValue { + err = vw.WriteNull() + if err != nil { + return err + } + continue + } + + err = currEncoder.EncodeValue(ec, vw, currVal) + if err != nil { + return err + } + } + return aw.WriteArrayEnd() +} + +func (dve DefaultValueEncoders) lookupElementEncoder(ec EncodeContext, origEncoder ValueEncoder, currVal reflect.Value) (ValueEncoder, reflect.Value, error) { + if origEncoder != nil || (currVal.Kind() != reflect.Interface) { + return origEncoder, currVal, nil + } + currVal = currVal.Elem() + if !currVal.IsValid() { + return nil, currVal, errInvalidValue + } + currEncoder, err := ec.LookupEncoder(currVal.Type()) + + return currEncoder, currVal, err +} + +// EmptyInterfaceEncodeValue is the ValueEncoderFunc for interface{}. +// +// Deprecated: EmptyInterfaceEncodeValue is not registered by default. Use EmptyInterfaceCodec.EncodeValue instead. +func (dve DefaultValueEncoders) EmptyInterfaceEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tEmpty { + return ValueEncoderError{Name: "EmptyInterfaceEncodeValue", Types: []reflect.Type{tEmpty}, Received: val} + } + + if val.IsNil() { + return vw.WriteNull() + } + encoder, err := ec.LookupEncoder(val.Elem().Type()) + if err != nil { + return err + } + + return encoder.EncodeValue(ec, vw, val.Elem()) +} + +// ValueMarshalerEncodeValue is the ValueEncoderFunc for ValueMarshaler implementations. +func (dve DefaultValueEncoders) ValueMarshalerEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + // Either val or a pointer to val must implement ValueMarshaler + switch { + case !val.IsValid(): + return ValueEncoderError{Name: "ValueMarshalerEncodeValue", Types: []reflect.Type{tValueMarshaler}, Received: val} + case val.Type().Implements(tValueMarshaler): + // If ValueMarshaler is implemented on a concrete type, make sure that val isn't a nil pointer + if isImplementationNil(val, tValueMarshaler) { + return vw.WriteNull() + } + case reflect.PtrTo(val.Type()).Implements(tValueMarshaler) && val.CanAddr(): + val = val.Addr() + default: + return ValueEncoderError{Name: "ValueMarshalerEncodeValue", Types: []reflect.Type{tValueMarshaler}, Received: val} + } + + fn := val.Convert(tValueMarshaler).MethodByName("MarshalBSONValue") + returns := fn.Call(nil) + if !returns[2].IsNil() { + return returns[2].Interface().(error) + } + t, data := returns[0].Interface().(bsontype.Type), returns[1].Interface().([]byte) + return bsonrw.Copier{}.CopyValueFromBytes(vw, t, data) +} + +// MarshalerEncodeValue is the ValueEncoderFunc for Marshaler implementations. +func (dve DefaultValueEncoders) MarshalerEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + // Either val or a pointer to val must implement Marshaler + switch { + case !val.IsValid(): + return ValueEncoderError{Name: "MarshalerEncodeValue", Types: []reflect.Type{tMarshaler}, Received: val} + case val.Type().Implements(tMarshaler): + // If Marshaler is implemented on a concrete type, make sure that val isn't a nil pointer + if isImplementationNil(val, tMarshaler) { + return vw.WriteNull() + } + case reflect.PtrTo(val.Type()).Implements(tMarshaler) && val.CanAddr(): + val = val.Addr() + default: + return ValueEncoderError{Name: "MarshalerEncodeValue", Types: []reflect.Type{tMarshaler}, Received: val} + } + + fn := val.Convert(tMarshaler).MethodByName("MarshalBSON") + returns := fn.Call(nil) + if !returns[1].IsNil() { + return returns[1].Interface().(error) + } + data := returns[0].Interface().([]byte) + return bsonrw.Copier{}.CopyValueFromBytes(vw, bsontype.EmbeddedDocument, data) +} + +// ProxyEncodeValue is the ValueEncoderFunc for Proxy implementations. +func (dve DefaultValueEncoders) ProxyEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + // Either val or a pointer to val must implement Proxy + switch { + case !val.IsValid(): + return ValueEncoderError{Name: "ProxyEncodeValue", Types: []reflect.Type{tProxy}, Received: val} + case val.Type().Implements(tProxy): + // If Proxy is implemented on a concrete type, make sure that val isn't a nil pointer + if isImplementationNil(val, tProxy) { + return vw.WriteNull() + } + case reflect.PtrTo(val.Type()).Implements(tProxy) && val.CanAddr(): + val = val.Addr() + default: + return ValueEncoderError{Name: "ProxyEncodeValue", Types: []reflect.Type{tProxy}, Received: val} + } + + fn := val.Convert(tProxy).MethodByName("ProxyBSON") + returns := fn.Call(nil) + if !returns[1].IsNil() { + return returns[1].Interface().(error) + } + data := returns[0] + var encoder ValueEncoder + var err error + if data.Elem().IsValid() { + encoder, err = ec.LookupEncoder(data.Elem().Type()) + } else { + encoder, err = ec.LookupEncoder(nil) + } + if err != nil { + return err + } + return encoder.EncodeValue(ec, vw, data.Elem()) +} + +// JavaScriptEncodeValue is the ValueEncoderFunc for the primitive.JavaScript type. +func (DefaultValueEncoders) JavaScriptEncodeValue(ectx EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tJavaScript { + return ValueEncoderError{Name: "JavaScriptEncodeValue", Types: []reflect.Type{tJavaScript}, Received: val} + } + + return vw.WriteJavascript(val.String()) +} + +// SymbolEncodeValue is the ValueEncoderFunc for the primitive.Symbol type. +func (DefaultValueEncoders) SymbolEncodeValue(ectx EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tSymbol { + return ValueEncoderError{Name: "SymbolEncodeValue", Types: []reflect.Type{tSymbol}, Received: val} + } + + return vw.WriteSymbol(val.String()) +} + +// BinaryEncodeValue is the ValueEncoderFunc for Binary. +func (DefaultValueEncoders) BinaryEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tBinary { + return ValueEncoderError{Name: "BinaryEncodeValue", Types: []reflect.Type{tBinary}, Received: val} + } + b := val.Interface().(primitive.Binary) + + return vw.WriteBinaryWithSubtype(b.Data, b.Subtype) +} + +// UndefinedEncodeValue is the ValueEncoderFunc for Undefined. +func (DefaultValueEncoders) UndefinedEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tUndefined { + return ValueEncoderError{Name: "UndefinedEncodeValue", Types: []reflect.Type{tUndefined}, Received: val} + } + + return vw.WriteUndefined() +} + +// DateTimeEncodeValue is the ValueEncoderFunc for DateTime. +func (DefaultValueEncoders) DateTimeEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tDateTime { + return ValueEncoderError{Name: "DateTimeEncodeValue", Types: []reflect.Type{tDateTime}, Received: val} + } + + return vw.WriteDateTime(val.Int()) +} + +// NullEncodeValue is the ValueEncoderFunc for Null. +func (DefaultValueEncoders) NullEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tNull { + return ValueEncoderError{Name: "NullEncodeValue", Types: []reflect.Type{tNull}, Received: val} + } + + return vw.WriteNull() +} + +// RegexEncodeValue is the ValueEncoderFunc for Regex. +func (DefaultValueEncoders) RegexEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tRegex { + return ValueEncoderError{Name: "RegexEncodeValue", Types: []reflect.Type{tRegex}, Received: val} + } + + regex := val.Interface().(primitive.Regex) + + return vw.WriteRegex(regex.Pattern, regex.Options) +} + +// DBPointerEncodeValue is the ValueEncoderFunc for DBPointer. +func (DefaultValueEncoders) DBPointerEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tDBPointer { + return ValueEncoderError{Name: "DBPointerEncodeValue", Types: []reflect.Type{tDBPointer}, Received: val} + } + + dbp := val.Interface().(primitive.DBPointer) + + return vw.WriteDBPointer(dbp.DB, dbp.Pointer) +} + +// TimestampEncodeValue is the ValueEncoderFunc for Timestamp. +func (DefaultValueEncoders) TimestampEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tTimestamp { + return ValueEncoderError{Name: "TimestampEncodeValue", Types: []reflect.Type{tTimestamp}, Received: val} + } + + ts := val.Interface().(primitive.Timestamp) + + return vw.WriteTimestamp(ts.T, ts.I) +} + +// MinKeyEncodeValue is the ValueEncoderFunc for MinKey. +func (DefaultValueEncoders) MinKeyEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tMinKey { + return ValueEncoderError{Name: "MinKeyEncodeValue", Types: []reflect.Type{tMinKey}, Received: val} + } + + return vw.WriteMinKey() +} + +// MaxKeyEncodeValue is the ValueEncoderFunc for MaxKey. +func (DefaultValueEncoders) MaxKeyEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tMaxKey { + return ValueEncoderError{Name: "MaxKeyEncodeValue", Types: []reflect.Type{tMaxKey}, Received: val} + } + + return vw.WriteMaxKey() +} + +// CoreDocumentEncodeValue is the ValueEncoderFunc for bsoncore.Document. +func (DefaultValueEncoders) CoreDocumentEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tCoreDocument { + return ValueEncoderError{Name: "CoreDocumentEncodeValue", Types: []reflect.Type{tCoreDocument}, Received: val} + } + + cdoc := val.Interface().(bsoncore.Document) + + return bsonrw.Copier{}.CopyDocumentFromBytes(vw, cdoc) +} + +// CodeWithScopeEncodeValue is the ValueEncoderFunc for CodeWithScope. +func (dve DefaultValueEncoders) CodeWithScopeEncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tCodeWithScope { + return ValueEncoderError{Name: "CodeWithScopeEncodeValue", Types: []reflect.Type{tCodeWithScope}, Received: val} + } + + cws := val.Interface().(primitive.CodeWithScope) + + dw, err := vw.WriteCodeWithScope(string(cws.Code)) + if err != nil { + return err + } + + sw := sliceWriterPool.Get().(*bsonrw.SliceWriter) + defer sliceWriterPool.Put(sw) + *sw = (*sw)[:0] + + scopeVW := bvwPool.Get(sw) + defer bvwPool.Put(scopeVW) + + encoder, err := ec.LookupEncoder(reflect.TypeOf(cws.Scope)) + if err != nil { + return err + } + + err = encoder.EncodeValue(ec, scopeVW, reflect.ValueOf(cws.Scope)) + if err != nil { + return err + } + + err = bsonrw.Copier{}.CopyBytesToDocumentWriter(dw, *sw) + if err != nil { + return err + } + return dw.WriteDocumentEnd() +} + +// isImplementationNil returns if val is a nil pointer and inter is implemented on a concrete type +func isImplementationNil(val reflect.Value, inter reflect.Type) bool { + vt := val.Type() + for vt.Kind() == reflect.Ptr { + vt = vt.Elem() + } + return vt.Implements(inter) && val.Kind() == reflect.Ptr && val.IsNil() +} diff --git a/mongo/bson/bsoncodec/default_value_encoders_test.go b/mongo/bson/bsoncodec/default_value_encoders_test.go new file mode 100644 index 0000000..0cb35a1 --- /dev/null +++ b/mongo/bson/bsoncodec/default_value_encoders_test.go @@ -0,0 +1,1909 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "net/url" + "reflect" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +type myInterface interface { + Foo() int +} + +type myStruct struct { + Val int +} + +func (ms myStruct) Foo() int { + return ms.Val +} + +func TestDefaultValueEncoders(t *testing.T) { + var dve DefaultValueEncoders + var wrong = func(string, string) string { return "wrong" } + + type mybool bool + type myint8 int8 + type myint16 int16 + type myint32 int32 + type myint64 int64 + type myint int + type myuint8 uint8 + type myuint16 uint16 + type myuint32 uint32 + type myuint64 uint64 + type myuint uint + type myfloat32 float32 + type myfloat64 float64 + + now := time.Now().Truncate(time.Millisecond) + pjsnum := new(json.Number) + *pjsnum = json.Number("3.14159") + d128 := primitive.NewDecimal128(12345, 67890) + var nilValueMarshaler *testValueMarshaler + var nilMarshaler *testMarshaler + var nilProxy *testProxy + + vmStruct := struct{ V testValueMarshalPtr }{testValueMarshalPtr{t: bsontype.String, buf: []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}}} + mStruct := struct{ V testMarshalPtr }{testMarshalPtr{buf: bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159))}} + pStruct := struct{ V testProxyPtr }{testProxyPtr{ret: int64(1234567890)}} + + type subtest struct { + name string + val interface{} + ectx *EncodeContext + llvrw *bsonrwtest.ValueReaderWriter + invoke bsonrwtest.Invoked + err error + } + + testCases := []struct { + name string + ve ValueEncoder + subtests []subtest + }{ + { + "BooleanEncodeValue", + ValueEncoderFunc(dve.BooleanEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "BooleanEncodeValue", Kinds: []reflect.Kind{reflect.Bool}, Received: reflect.ValueOf(wrong)}, + }, + {"fast path", bool(true), nil, nil, bsonrwtest.WriteBoolean, nil}, + {"reflection path", mybool(true), nil, nil, bsonrwtest.WriteBoolean, nil}, + }, + }, + { + "IntEncodeValue", + ValueEncoderFunc(dve.IntEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "IntEncodeValue", + Kinds: []reflect.Kind{reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int}, + Received: reflect.ValueOf(wrong), + }, + }, + {"int8/fast path", int8(127), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int16/fast path", int16(32767), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int32/fast path", int32(2147483647), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int64/fast path", int64(1234567890987), nil, nil, bsonrwtest.WriteInt64, nil}, + {"int64/fast path - minsize", int64(math.MaxInt32), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"int64/fast path - minsize too large", int64(math.MaxInt32 + 1), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"int64/fast path - minsize too small", int64(math.MinInt32 - 1), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"int/fast path - positive int32", int(math.MaxInt32 - 1), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/fast path - negative int32", int(math.MinInt32 + 1), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/fast path - MaxInt32", int(math.MaxInt32), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/fast path - MinInt32", int(math.MinInt32), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int8/reflection path", myint8(127), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int16/reflection path", myint16(32767), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int32/reflection path", myint32(2147483647), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int64/reflection path", myint64(1234567890987), nil, nil, bsonrwtest.WriteInt64, nil}, + {"int64/reflection path - minsize", myint64(math.MaxInt32), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"int64/reflection path - minsize too large", myint64(math.MaxInt32 + 1), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"int64/reflection path - minsize too small", myint64(math.MinInt32 - 1), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"int/reflection path - positive int32", myint(math.MaxInt32 - 1), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/reflection path - negative int32", myint(math.MinInt32 + 1), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/reflection path - MaxInt32", myint(math.MaxInt32), nil, nil, bsonrwtest.WriteInt32, nil}, + {"int/reflection path - MinInt32", myint(math.MinInt32), nil, nil, bsonrwtest.WriteInt32, nil}, + }, + }, + { + "UintEncodeValue", + defaultUIntCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "UintEncodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.ValueOf(wrong), + }, + }, + {"uint8/fast path", uint8(127), nil, nil, bsonrwtest.WriteInt32, nil}, + {"uint16/fast path", uint16(32767), nil, nil, bsonrwtest.WriteInt32, nil}, + {"uint32/fast path", uint32(2147483647), nil, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/fast path", uint64(1234567890987), nil, nil, bsonrwtest.WriteInt64, nil}, + {"uint/fast path", uint(1234567), nil, nil, bsonrwtest.WriteInt64, nil}, + {"uint32/fast path - minsize", uint32(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint64/fast path - minsize", uint64(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint/fast path - minsize", uint(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint32/fast path - minsize too large", uint32(2147483648), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/fast path - minsize too large", uint64(2147483648), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint/fast path - minsize too large", uint(2147483648), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/fast path - overflow", uint64(1 << 63), nil, nil, bsonrwtest.Nothing, fmt.Errorf("%d overflows int64", uint64(1<<63))}, + {"uint8/reflection path", myuint8(127), nil, nil, bsonrwtest.WriteInt32, nil}, + {"uint16/reflection path", myuint16(32767), nil, nil, bsonrwtest.WriteInt32, nil}, + {"uint32/reflection path", myuint32(2147483647), nil, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/reflection path", myuint64(1234567890987), nil, nil, bsonrwtest.WriteInt64, nil}, + {"uint32/reflection path - minsize", myuint32(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint64/reflection path - minsize", myuint64(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint/reflection path - minsize", myuint(2147483647), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt32, nil}, + {"uint32/reflection path - minsize too large", myuint(1 << 31), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/reflection path - minsize too large", myuint64(1 << 31), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint/reflection path - minsize too large", myuint(2147483648), &EncodeContext{MinSize: true}, nil, bsonrwtest.WriteInt64, nil}, + {"uint64/reflection path - overflow", myuint64(1 << 63), nil, nil, bsonrwtest.Nothing, fmt.Errorf("%d overflows int64", uint64(1<<63))}, + }, + }, + { + "FloatEncodeValue", + ValueEncoderFunc(dve.FloatEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "FloatEncodeValue", + Kinds: []reflect.Kind{reflect.Float32, reflect.Float64}, + Received: reflect.ValueOf(wrong), + }, + }, + {"float32/fast path", float32(3.14159), nil, nil, bsonrwtest.WriteDouble, nil}, + {"float64/fast path", float64(3.14159), nil, nil, bsonrwtest.WriteDouble, nil}, + {"float32/reflection path", myfloat32(3.14159), nil, nil, bsonrwtest.WriteDouble, nil}, + {"float64/reflection path", myfloat64(3.14159), nil, nil, bsonrwtest.WriteDouble, nil}, + }, + }, + { + "TimeEncodeValue", + defaultTimeCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "TimeEncodeValue", Types: []reflect.Type{tTime}, Received: reflect.ValueOf(wrong)}, + }, + {"time.Time", now, nil, nil, bsonrwtest.WriteDateTime, nil}, + }, + }, + { + "MapEncodeValue", + defaultMapCodec, + []subtest{ + { + "wrong kind", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "MapEncodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: reflect.ValueOf(wrong)}, + }, + { + "WriteDocument Error", + map[string]interface{}{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wd error"), ErrAfter: bsonrwtest.WriteDocument}, + bsonrwtest.WriteDocument, + errors.New("wd error"), + }, + { + "Lookup Error", + map[string]int{"foo": 1}, + &EncodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteDocument, + fmt.Errorf("no encoder found for int"), + }, + { + "WriteDocumentElement Error", + map[string]interface{}{"foo": "bar"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wde error"), ErrAfter: bsonrwtest.WriteDocumentElement}, + bsonrwtest.WriteDocumentElement, + errors.New("wde error"), + }, + { + "EncodeValue Error", + map[string]interface{}{"foo": "bar"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ev error"), ErrAfter: bsonrwtest.WriteString}, + bsonrwtest.WriteString, + errors.New("ev error"), + }, + { + "empty map/success", + map[string]interface{}{}, + &EncodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "with interface/success", + map[string]myInterface{"foo": myStruct{1}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "with interface/nil/success", + map[string]myInterface{"foo": nil}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "non-string key success", + map[int]interface{}{ + 1: "foobar", + }, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteDocumentEnd, + nil, + }, + }, + }, + { + "ArrayEncodeValue", + ValueEncoderFunc(dve.ArrayEncodeValue), + []subtest{ + { + "wrong kind", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "ArrayEncodeValue", Kinds: []reflect.Kind{reflect.Array}, Received: reflect.ValueOf(wrong)}, + }, + { + "WriteArray Error", + [1]string{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wa error"), ErrAfter: bsonrwtest.WriteArray}, + bsonrwtest.WriteArray, + errors.New("wa error"), + }, + { + "Lookup Error", + [1]int{1}, + &EncodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteArray, + fmt.Errorf("no encoder found for int"), + }, + { + "WriteArrayElement Error", + [1]string{"foo"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wae error"), ErrAfter: bsonrwtest.WriteArrayElement}, + bsonrwtest.WriteArrayElement, + errors.New("wae error"), + }, + { + "EncodeValue Error", + [1]string{"foo"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ev error"), ErrAfter: bsonrwtest.WriteString}, + bsonrwtest.WriteString, + errors.New("ev error"), + }, + { + "[1]primitive.E/success", + [1]primitive.E{{"hello", "world"}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "[1]primitive.E/success", + [1]primitive.E{{"hello", nil}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "[1]interface/success", + [1]myInterface{myStruct{1}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteArrayEnd, + nil, + }, + { + "[1]interface/nil/success", + [1]myInterface{nil}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteArrayEnd, + nil, + }, + }, + }, + { + "SliceEncodeValue", + defaultSliceCodec, + []subtest{ + { + "wrong kind", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "SliceEncodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: reflect.ValueOf(wrong)}, + }, + { + "WriteArray Error", + []string{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wa error"), ErrAfter: bsonrwtest.WriteArray}, + bsonrwtest.WriteArray, + errors.New("wa error"), + }, + { + "Lookup Error", + []int{1}, + &EncodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteArray, + fmt.Errorf("no encoder found for int"), + }, + { + "WriteArrayElement Error", + []string{"foo"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wae error"), ErrAfter: bsonrwtest.WriteArrayElement}, + bsonrwtest.WriteArrayElement, + errors.New("wae error"), + }, + { + "EncodeValue Error", + []string{"foo"}, + &EncodeContext{Registry: buildDefaultRegistry()}, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ev error"), ErrAfter: bsonrwtest.WriteString}, + bsonrwtest.WriteString, + errors.New("ev error"), + }, + { + "D/success", + primitive.D{{"hello", "world"}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "D/success", + primitive.D{{"hello", nil}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "empty slice/success", + []interface{}{}, + &EncodeContext{Registry: NewRegistryBuilder().Build()}, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteArrayEnd, + nil, + }, + { + "interface/success", + []myInterface{myStruct{1}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteArrayEnd, + nil, + }, + { + "interface/success", + []myInterface{nil}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteArrayEnd, + nil, + }, + }, + }, + { + "ObjectIDEncodeValue", + ValueEncoderFunc(dve.ObjectIDEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "ObjectIDEncodeValue", Types: []reflect.Type{tOID}, Received: reflect.ValueOf(wrong)}, + }, + { + "primitive.ObjectID/success", + primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + nil, nil, bsonrwtest.WriteObjectID, nil, + }, + }, + }, + { + "Decimal128EncodeValue", + ValueEncoderFunc(dve.Decimal128EncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "Decimal128EncodeValue", Types: []reflect.Type{tDecimal}, Received: reflect.ValueOf(wrong)}, + }, + {"Decimal128/success", d128, nil, nil, bsonrwtest.WriteDecimal128, nil}, + }, + }, + { + "JSONNumberEncodeValue", + ValueEncoderFunc(dve.JSONNumberEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "JSONNumberEncodeValue", Types: []reflect.Type{tJSONNumber}, Received: reflect.ValueOf(wrong)}, + }, + { + "json.Number/invalid", + json.Number("hello world"), + nil, nil, bsonrwtest.Nothing, errors.New(`strconv.ParseFloat: parsing "hello world": invalid syntax`), + }, + { + "json.Number/int64/success", + json.Number("1234567890"), + nil, nil, bsonrwtest.WriteInt64, nil, + }, + { + "json.Number/float64/success", + json.Number("3.14159"), + nil, nil, bsonrwtest.WriteDouble, nil, + }, + }, + }, + { + "URLEncodeValue", + ValueEncoderFunc(dve.URLEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "URLEncodeValue", Types: []reflect.Type{tURL}, Received: reflect.ValueOf(wrong)}, + }, + {"url.URL", url.URL{Scheme: "http", Host: "example.com"}, nil, nil, bsonrwtest.WriteString, nil}, + }, + }, + { + "ByteSliceEncodeValue", + defaultByteSliceCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "ByteSliceEncodeValue", Types: []reflect.Type{tByteSlice}, Received: reflect.ValueOf(wrong)}, + }, + {"[]byte", []byte{0x01, 0x02, 0x03}, nil, nil, bsonrwtest.WriteBinary, nil}, + {"[]byte/nil", []byte(nil), nil, nil, bsonrwtest.WriteNull, nil}, + }, + }, + { + "EmptyInterfaceEncodeValue", + defaultEmptyInterfaceCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "EmptyInterfaceEncodeValue", Types: []reflect.Type{tEmpty}, Received: reflect.ValueOf(wrong)}, + }, + }, + }, + { + "ValueMarshalerEncodeValue", + ValueEncoderFunc(dve.ValueMarshalerEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "ValueMarshalerEncodeValue", + Types: []reflect.Type{tValueMarshaler}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "MarshalBSONValue error", + testValueMarshaler{err: errors.New("mbsonv error")}, + nil, + nil, + bsonrwtest.Nothing, + errors.New("mbsonv error"), + }, + { + "Copy error", + testValueMarshaler{}, + nil, + nil, + bsonrwtest.Nothing, + fmt.Errorf("Cannot copy unknown BSON type %s", bsontype.Type(0)), + }, + { + "success struct implementation", + testValueMarshaler{t: bsontype.String, buf: []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}}, + nil, + nil, + bsonrwtest.WriteString, + nil, + }, + { + "success ptr to struct implementation", + &testValueMarshaler{t: bsontype.String, buf: []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}}, + nil, + nil, + bsonrwtest.WriteString, + nil, + }, + { + "success nil ptr to struct implementation", + nilValueMarshaler, + nil, + nil, + bsonrwtest.WriteNull, + nil, + }, + { + "success ptr to ptr implementation", + &testValueMarshalPtr{t: bsontype.String, buf: []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}}, + nil, + nil, + bsonrwtest.WriteString, + nil, + }, + { + "unaddressable ptr implementation", + testValueMarshalPtr{t: bsontype.String, buf: []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}}, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "ValueMarshalerEncodeValue", + Types: []reflect.Type{tValueMarshaler}, + Received: reflect.ValueOf(testValueMarshalPtr{}), + }, + }, + }, + }, + { + "MarshalerEncodeValue", + ValueEncoderFunc(dve.MarshalerEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "MarshalerEncodeValue", Types: []reflect.Type{tMarshaler}, Received: reflect.ValueOf(wrong)}, + }, + { + "MarshalBSON error", + testMarshaler{err: errors.New("mbson error")}, + nil, + nil, + bsonrwtest.Nothing, + errors.New("mbson error"), + }, + { + "success struct implementation", + testMarshaler{buf: bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159))}, + nil, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "success ptr to struct implementation", + &testMarshaler{buf: bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159))}, + nil, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "success nil ptr to struct implementation", + nilMarshaler, + nil, + nil, + bsonrwtest.WriteNull, + nil, + }, + { + "success ptr to ptr implementation", + &testMarshalPtr{buf: bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159))}, + nil, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "unaddressable ptr implementation", + testMarshalPtr{buf: bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159))}, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "MarshalerEncodeValue", Types: []reflect.Type{tMarshaler}, Received: reflect.ValueOf(testMarshalPtr{})}, + }, + }, + }, + { + "ProxyEncodeValue", + ValueEncoderFunc(dve.ProxyEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "ProxyEncodeValue", Types: []reflect.Type{tProxy}, Received: reflect.ValueOf(wrong)}, + }, + { + "Proxy error", + testProxy{err: errors.New("proxy error")}, + nil, + nil, + bsonrwtest.Nothing, + errors.New("proxy error"), + }, + { + "Lookup error", + testProxy{ret: nil}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.Nothing, + ErrNoEncoder{Type: nil}, + }, + { + "success struct implementation", + testProxy{ret: int64(1234567890)}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteInt64, + nil, + }, + { + "success ptr to struct implementation", + &testProxy{ret: int64(1234567890)}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteInt64, + nil, + }, + { + "success nil ptr to struct implementation", + nilProxy, + nil, + nil, + bsonrwtest.WriteNull, + nil, + }, + { + "success ptr to ptr implementation", + &testProxyPtr{ret: int64(1234567890)}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteInt64, + nil, + }, + { + "unaddressable ptr implementation", + testProxyPtr{ret: int64(1234567890)}, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "ProxyEncodeValue", Types: []reflect.Type{tProxy}, Received: reflect.ValueOf(testProxyPtr{})}, + }, + }, + }, + { + "PointerCodec.EncodeValue", + NewPointerCodec(), + []subtest{ + { + "nil", + nil, + nil, + nil, + bsonrwtest.WriteNull, + nil, + }, + { + "not pointer", + int32(123456), + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "PointerCodec.EncodeValue", Kinds: []reflect.Kind{reflect.Ptr}, Received: reflect.ValueOf(int32(123456))}, + }, + { + "typed nil", + (*int32)(nil), + nil, + nil, + bsonrwtest.WriteNull, + nil, + }, + { + "no encoder", + &wrong, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.Nothing, + ErrNoEncoder{Type: reflect.TypeOf(wrong)}, + }, + }, + }, + { + "pointer implementation addressable interface", + NewPointerCodec(), + []subtest{ + { + "ValueMarshaler", + &vmStruct, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "Marshaler", + &mStruct, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "Proxy", + &pStruct, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + }, + }, + { + "JavaScriptEncodeValue", + ValueEncoderFunc(dve.JavaScriptEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "JavaScriptEncodeValue", Types: []reflect.Type{tJavaScript}, Received: reflect.ValueOf(wrong)}, + }, + {"JavaScript", primitive.JavaScript("foobar"), nil, nil, bsonrwtest.WriteJavascript, nil}, + }, + }, + { + "SymbolEncodeValue", + ValueEncoderFunc(dve.SymbolEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "SymbolEncodeValue", Types: []reflect.Type{tSymbol}, Received: reflect.ValueOf(wrong)}, + }, + {"Symbol", primitive.Symbol("foobar"), nil, nil, bsonrwtest.WriteSymbol, nil}, + }, + }, + { + "BinaryEncodeValue", + ValueEncoderFunc(dve.BinaryEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "BinaryEncodeValue", Types: []reflect.Type{tBinary}, Received: reflect.ValueOf(wrong)}, + }, + {"Binary/success", primitive.Binary{Data: []byte{0x01, 0x02}, Subtype: 0xFF}, nil, nil, bsonrwtest.WriteBinaryWithSubtype, nil}, + }, + }, + { + "UndefinedEncodeValue", + ValueEncoderFunc(dve.UndefinedEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "UndefinedEncodeValue", Types: []reflect.Type{tUndefined}, Received: reflect.ValueOf(wrong)}, + }, + {"Undefined/success", primitive.Undefined{}, nil, nil, bsonrwtest.WriteUndefined, nil}, + }, + }, + { + "DateTimeEncodeValue", + ValueEncoderFunc(dve.DateTimeEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "DateTimeEncodeValue", Types: []reflect.Type{tDateTime}, Received: reflect.ValueOf(wrong)}, + }, + {"DateTime/success", primitive.DateTime(1234567890), nil, nil, bsonrwtest.WriteDateTime, nil}, + }, + }, + { + "NullEncodeValue", + ValueEncoderFunc(dve.NullEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "NullEncodeValue", Types: []reflect.Type{tNull}, Received: reflect.ValueOf(wrong)}, + }, + {"Null/success", primitive.Null{}, nil, nil, bsonrwtest.WriteNull, nil}, + }, + }, + { + "RegexEncodeValue", + ValueEncoderFunc(dve.RegexEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "RegexEncodeValue", Types: []reflect.Type{tRegex}, Received: reflect.ValueOf(wrong)}, + }, + {"Regex/success", primitive.Regex{Pattern: "foo", Options: "bar"}, nil, nil, bsonrwtest.WriteRegex, nil}, + }, + }, + { + "DBPointerEncodeValue", + ValueEncoderFunc(dve.DBPointerEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "DBPointerEncodeValue", Types: []reflect.Type{tDBPointer}, Received: reflect.ValueOf(wrong)}, + }, + { + "DBPointer/success", + primitive.DBPointer{ + DB: "foobar", + Pointer: primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C}, + }, + nil, nil, bsonrwtest.WriteDBPointer, nil, + }, + }, + }, + { + "TimestampEncodeValue", + ValueEncoderFunc(dve.TimestampEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "TimestampEncodeValue", Types: []reflect.Type{tTimestamp}, Received: reflect.ValueOf(wrong)}, + }, + {"Timestamp/success", primitive.Timestamp{T: 12345, I: 67890}, nil, nil, bsonrwtest.WriteTimestamp, nil}, + }, + }, + { + "MinKeyEncodeValue", + ValueEncoderFunc(dve.MinKeyEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "MinKeyEncodeValue", Types: []reflect.Type{tMinKey}, Received: reflect.ValueOf(wrong)}, + }, + {"MinKey/success", primitive.MinKey{}, nil, nil, bsonrwtest.WriteMinKey, nil}, + }, + }, + { + "MaxKeyEncodeValue", + ValueEncoderFunc(dve.MaxKeyEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{Name: "MaxKeyEncodeValue", Types: []reflect.Type{tMaxKey}, Received: reflect.ValueOf(wrong)}, + }, + {"MaxKey/success", primitive.MaxKey{}, nil, nil, bsonrwtest.WriteMaxKey, nil}, + }, + }, + { + "CoreDocumentEncodeValue", + ValueEncoderFunc(dve.CoreDocumentEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "CoreDocumentEncodeValue", + Types: []reflect.Type{tCoreDocument}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "WriteDocument Error", + bsoncore.Document{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wd error"), ErrAfter: bsonrwtest.WriteDocument}, + bsonrwtest.WriteDocument, + errors.New("wd error"), + }, + { + "bsoncore.Document.Elements Error", + bsoncore.Document{0xFF, 0x00, 0x00, 0x00, 0x00}, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteDocument, + errors.New("length read exceeds number of bytes available. length=5 bytes=255"), + }, + { + "WriteDocumentElement Error", + bsoncore.Document(buildDocument(bsoncore.AppendNullElement(nil, "foo"))), + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wde error"), ErrAfter: bsonrwtest.WriteDocumentElement}, + bsonrwtest.WriteDocumentElement, + errors.New("wde error"), + }, + { + "encodeValue error", + bsoncore.Document(buildDocument(bsoncore.AppendNullElement(nil, "foo"))), + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ev error"), ErrAfter: bsonrwtest.WriteNull}, + bsonrwtest.WriteNull, + errors.New("ev error"), + }, + { + "iterator error", + bsoncore.Document{0x0C, 0x00, 0x00, 0x00, 0x01, 'f', 'o', 'o', 0x00, 0x01, 0x02, 0x03}, + nil, + &bsonrwtest.ValueReaderWriter{}, + bsonrwtest.WriteDocumentElement, + errors.New("not enough bytes available to read type. bytes=3 type=double"), + }, + }, + }, + { + "StructEncodeValue", + defaultTestStructCodec, + []subtest{ + { + "interface value", + struct{ Foo myInterface }{Foo: myStruct{1}}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + { + "nil interface value", + struct{ Foo myInterface }{Foo: nil}, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, + bsonrwtest.WriteDocumentEnd, + nil, + }, + }, + }, + { + "CodeWithScopeEncodeValue", + ValueEncoderFunc(dve.CodeWithScopeEncodeValue), + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "CodeWithScopeEncodeValue", + Types: []reflect.Type{tCodeWithScope}, + Received: reflect.ValueOf(wrong), + }, + }, + { + "WriteCodeWithScope error", + primitive.CodeWithScope{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wcws error"), ErrAfter: bsonrwtest.WriteCodeWithScope}, + bsonrwtest.WriteCodeWithScope, + errors.New("wcws error"), + }, + { + "CodeWithScope/success", + primitive.CodeWithScope{ + Code: "var hello = 'world';", + Scope: primitive.D{}, + }, + &EncodeContext{Registry: buildDefaultRegistry()}, + nil, bsonrwtest.WriteDocumentEnd, nil, + }, + }, + }, + { + "CoreArrayEncodeValue", + defaultArrayCodec, + []subtest{ + { + "wrong type", + wrong, + nil, + nil, + bsonrwtest.Nothing, + ValueEncoderError{ + Name: "CoreArrayEncodeValue", + Types: []reflect.Type{tCoreArray}, + Received: reflect.ValueOf(wrong), + }, + }, + + { + "WriteArray Error", + bsoncore.Array{}, + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wa error"), ErrAfter: bsonrwtest.WriteArray}, + bsonrwtest.WriteArray, + errors.New("wa error"), + }, + { + "WriteArrayElement Error", + bsoncore.Array(buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendNullElement(nil, "foo") + })), + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("wae error"), ErrAfter: bsonrwtest.WriteArrayElement}, + bsonrwtest.WriteArrayElement, + errors.New("wae error"), + }, + { + "encodeValue error", + bsoncore.Array(buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendNullElement(nil, "foo") + })), + nil, + &bsonrwtest.ValueReaderWriter{Err: errors.New("ev error"), ErrAfter: bsonrwtest.WriteNull}, + bsonrwtest.WriteNull, + errors.New("ev error"), + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for _, subtest := range tc.subtests { + t.Run(subtest.name, func(t *testing.T) { + var ec EncodeContext + if subtest.ectx != nil { + ec = *subtest.ectx + } + llvrw := new(bsonrwtest.ValueReaderWriter) + if subtest.llvrw != nil { + llvrw = subtest.llvrw + } + llvrw.T = t + err := tc.ve.EncodeValue(ec, llvrw, reflect.ValueOf(subtest.val)) + if !compareErrors(err, subtest.err) { + t.Errorf("Errors do not match. got %v; want %v", err, subtest.err) + } + invoked := llvrw.Invoked + if !cmp.Equal(invoked, subtest.invoke) { + t.Errorf("Incorrect method invoked. got %v; want %v", invoked, subtest.invoke) + } + }) + } + }) + } + + t.Run("success path", func(t *testing.T) { + oid := primitive.NewObjectID() + oids := []primitive.ObjectID{primitive.NewObjectID(), primitive.NewObjectID(), primitive.NewObjectID()} + var str = new(string) + *str = "bar" + now := time.Now().Truncate(time.Millisecond) + murl, err := url.Parse("https://mongodb.com/random-url?hello=world") + if err != nil { + t.Errorf("Error parsing URL: %v", err) + t.FailNow() + } + decimal128, err := primitive.ParseDecimal128("1.5e10") + if err != nil { + t.Errorf("Error parsing decimal128: %v", err) + t.FailNow() + } + + testCases := []struct { + name string + value interface{} + b []byte + err error + }{ + { + "map[string]int", + map[string]int32{"foo": 1}, + []byte{ + 0x0E, 0x00, 0x00, 0x00, + 0x10, 'f', 'o', 'o', 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x00, + }, + nil, + }, + { + "map[string]primitive.ObjectID", + map[string]primitive.ObjectID{"foo": oid}, + buildDocument(bsoncore.AppendObjectIDElement(nil, "foo", oid)), + nil, + }, + { + "map[string][]int32", + map[string][]int32{"Z": {1, 2, 3}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt32Element(doc, "0", 1) + doc = bsoncore.AppendInt32Element(doc, "1", 2) + return bsoncore.AppendInt32Element(doc, "2", 3) + }), + nil, + }, + { + "map[string][]primitive.ObjectID", + map[string][]primitive.ObjectID{"Z": oids}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendObjectIDElement(doc, "0", oids[0]) + doc = bsoncore.AppendObjectIDElement(doc, "1", oids[1]) + return bsoncore.AppendObjectIDElement(doc, "2", oids[2]) + }), + nil, + }, + { + "map[string][]json.Number(int64)", + map[string][]json.Number{"Z": {json.Number("5"), json.Number("10")}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt64Element(doc, "0", 5) + return bsoncore.AppendInt64Element(doc, "1", 10) + }), + nil, + }, + { + "map[string][]json.Number(float64)", + map[string][]json.Number{"Z": {json.Number("5"), json.Number("10.1")}}, + buildDocumentArray(func(doc []byte) []byte { + doc = bsoncore.AppendInt64Element(doc, "0", 5) + return bsoncore.AppendDoubleElement(doc, "1", 10.1) + }), + nil, + }, + { + "map[string][]*url.URL", + map[string][]*url.URL{"Z": {murl}}, + buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendStringElement(doc, "0", murl.String()) + }), + nil, + }, + { + "map[string][]primitive.Decimal128", + map[string][]primitive.Decimal128{"Z": {decimal128}}, + buildDocumentArray(func(doc []byte) []byte { + return bsoncore.AppendDecimal128Element(doc, "0", decimal128) + }), + nil, + }, + { + "-", + struct { + A string `bson:"-"` + }{ + A: "", + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitempty", + struct { + A string `bson:",omitempty"` + }{ + A: "", + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitempty, empty time", + struct { + A time.Time `bson:",omitempty"` + }{ + A: time.Time{}, + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "no private fields", + noPrivateFields{a: "should be empty"}, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "minsize", + struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline", + struct { + Foo struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Foo: struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline struct pointer", + struct { + Foo *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + Bar *struct { + B int64 + } `bson:",inline"` + }{ + Foo: &struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + Bar: nil, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "nested inline struct pointer", + struct { + Foo *struct { + Bar *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + } `bson:",inline"` + }{ + Foo: &struct { + Bar *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Bar: &struct { + A int64 `bson:",minsize"` + }{ + A: 12345, + }, + }, + }, + buildDocument(bsoncore.AppendInt32Element(nil, "a", 12345)), + nil, + }, + { + "inline nil struct pointer", + struct { + Foo *struct { + A int64 `bson:",minsize"` + } `bson:",inline"` + }{ + Foo: nil, + }, + buildDocument([]byte{}), + nil, + }, + { + "inline overwrite", + struct { + Foo struct { + A int32 + B string + } `bson:",inline"` + A int64 + }{ + Foo: struct { + A int32 + B string + }{ + A: 0, + B: "foo", + }, + A: 54321, + }, + buildDocument(func(doc []byte) []byte { + doc = bsoncore.AppendStringElement(doc, "b", "foo") + doc = bsoncore.AppendInt64Element(doc, "a", 54321) + return doc + }(nil)), + nil, + }, + { + "inline overwrite respects ordering", + struct { + A int64 + Foo struct { + A int32 + B string + } `bson:",inline"` + }{ + A: 54321, + Foo: struct { + A int32 + B string + }{ + A: 0, + B: "foo", + }, + }, + buildDocument(func(doc []byte) []byte { + doc = bsoncore.AppendInt64Element(doc, "a", 54321) + doc = bsoncore.AppendStringElement(doc, "b", "foo") + return doc + }(nil)), + nil, + }, + { + "inline overwrite with nested structs", + struct { + Foo struct { + A int32 + } `bson:",inline"` + Bar struct { + A int32 + } `bson:",inline"` + A int64 + }{ + Foo: struct { + A int32 + }{}, + Bar: struct { + A int32 + }{}, + A: 54321, + }, + buildDocument(bsoncore.AppendInt64Element(nil, "a", 54321)), + nil, + }, + { + "inline map", + struct { + Foo map[string]string `bson:",inline"` + }{ + Foo: map[string]string{"foo": "bar"}, + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "alternate name bson:name", + struct { + A string `bson:"foo"` + }{ + A: "bar", + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "alternate name", + struct { + A string `bson:"foo"` + }{ + A: "bar", + }, + buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar")), + nil, + }, + { + "inline, omitempty", + struct { + A string + Foo zeroTest `bson:"omitempty,inline"` + }{ + A: "bar", + Foo: zeroTest{true}, + }, + buildDocument(bsoncore.AppendStringElement(nil, "a", "bar")), + nil, + }, + { + "struct{}", + struct { + A bool + B int32 + C int64 + D uint16 + E uint64 + F float64 + G string + H map[string]string + I []byte + K [2]string + L struct { + M string + } + Q primitive.ObjectID + T []struct{} + Y json.Number + Z time.Time + AA json.Number + AB *url.URL + AC primitive.Decimal128 + AD *time.Time + AE testValueMarshaler + AF Proxy + AG testProxy + AH map[string]interface{} + AI primitive.CodeWithScope + }{ + A: true, + B: 123, + C: 456, + D: 789, + E: 101112, + F: 3.14159, + G: "Hello, world", + H: map[string]string{"foo": "bar"}, + I: []byte{0x01, 0x02, 0x03}, + K: [2]string{"baz", "qux"}, + L: struct { + M string + }{ + M: "foobar", + }, + Q: oid, + T: nil, + Y: json.Number("5"), + Z: now, + AA: json.Number("10.1"), + AB: murl, + AC: decimal128, + AD: &now, + AE: testValueMarshaler{t: bsontype.String, buf: bsoncore.AppendString(nil, "hello, world")}, + AF: testProxy{ret: struct{ Hello string }{Hello: "world!"}}, + AG: testProxy{ret: struct{ Pi float64 }{Pi: 3.14159}}, + AH: nil, + AI: primitive.CodeWithScope{Code: "var hello = 'world';", Scope: primitive.D{{"pi", 3.14159}}}, + }, + buildDocument(func(doc []byte) []byte { + doc = bsoncore.AppendBooleanElement(doc, "a", true) + doc = bsoncore.AppendInt32Element(doc, "b", 123) + doc = bsoncore.AppendInt64Element(doc, "c", 456) + doc = bsoncore.AppendInt32Element(doc, "d", 789) + doc = bsoncore.AppendInt64Element(doc, "e", 101112) + doc = bsoncore.AppendDoubleElement(doc, "f", 3.14159) + doc = bsoncore.AppendStringElement(doc, "g", "Hello, world") + doc = bsoncore.AppendDocumentElement(doc, "h", buildDocument(bsoncore.AppendStringElement(nil, "foo", "bar"))) + doc = bsoncore.AppendBinaryElement(doc, "i", 0x00, []byte{0x01, 0x02, 0x03}) + doc = bsoncore.AppendArrayElement(doc, "k", + buildArray(bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "baz"), "1", "qux")), + ) + doc = bsoncore.AppendDocumentElement(doc, "l", buildDocument(bsoncore.AppendStringElement(nil, "m", "foobar"))) + doc = bsoncore.AppendObjectIDElement(doc, "q", oid) + doc = bsoncore.AppendNullElement(doc, "t") + doc = bsoncore.AppendInt64Element(doc, "y", 5) + doc = bsoncore.AppendDateTimeElement(doc, "z", now.UnixNano()/int64(time.Millisecond)) + doc = bsoncore.AppendDoubleElement(doc, "aa", 10.1) + doc = bsoncore.AppendStringElement(doc, "ab", murl.String()) + doc = bsoncore.AppendDecimal128Element(doc, "ac", decimal128) + doc = bsoncore.AppendDateTimeElement(doc, "ad", now.UnixNano()/int64(time.Millisecond)) + doc = bsoncore.AppendStringElement(doc, "ae", "hello, world") + doc = bsoncore.AppendDocumentElement(doc, "af", buildDocument(bsoncore.AppendStringElement(nil, "hello", "world!"))) + doc = bsoncore.AppendDocumentElement(doc, "ag", buildDocument(bsoncore.AppendDoubleElement(nil, "pi", 3.14159))) + doc = bsoncore.AppendNullElement(doc, "ah") + doc = bsoncore.AppendCodeWithScopeElement(doc, "ai", + "var hello = 'world';", buildDocument(bsoncore.AppendDoubleElement(nil, "pi", 3.14159)), + ) + return doc + }(nil)), + nil, + }, + { + "struct{[]interface{}}", + struct { + A []bool + B []int32 + C []int64 + D []uint16 + E []uint64 + F []float64 + G []string + H []map[string]string + I [][]byte + K [1][2]string + L []struct { + M string + } + N [][]string + R []primitive.ObjectID + T []struct{} + W []map[string]struct{} + X []map[string]struct{} + Y []map[string]struct{} + Z []time.Time + AA []json.Number + AB []*url.URL + AC []primitive.Decimal128 + AD []*time.Time + AE []testValueMarshaler + AF []Proxy + AG []testProxy + }{ + A: []bool{true}, + B: []int32{123}, + C: []int64{456}, + D: []uint16{789}, + E: []uint64{101112}, + F: []float64{3.14159}, + G: []string{"Hello, world"}, + H: []map[string]string{{"foo": "bar"}}, + I: [][]byte{{0x01, 0x02, 0x03}}, + K: [1][2]string{{"baz", "qux"}}, + L: []struct { + M string + }{ + { + M: "foobar", + }, + }, + N: [][]string{{"foo", "bar"}}, + R: oids, + T: nil, + W: nil, + X: []map[string]struct{}{}, // Should be empty BSON Array + Y: []map[string]struct{}{{}}, // Should be BSON array with one element, an empty BSON SubDocument + Z: []time.Time{now, now}, + AA: []json.Number{json.Number("5"), json.Number("10.1")}, + AB: []*url.URL{murl}, + AC: []primitive.Decimal128{decimal128}, + AD: []*time.Time{&now, &now}, + AE: []testValueMarshaler{ + {t: bsontype.String, buf: bsoncore.AppendString(nil, "hello")}, + {t: bsontype.String, buf: bsoncore.AppendString(nil, "world")}, + }, + AF: []Proxy{ + testProxy{ret: struct{ Hello string }{Hello: "world!"}}, + testProxy{ret: struct{ Foo string }{Foo: "bar"}}, + }, + AG: []testProxy{ + {ret: struct{ One int64 }{One: 1234567890}}, + {ret: struct{ Pi float64 }{Pi: 3.14159}}, + }, + }, + buildDocument(func(doc []byte) []byte { + doc = appendArrayElement(doc, "a", bsoncore.AppendBooleanElement(nil, "0", true)) + doc = appendArrayElement(doc, "b", bsoncore.AppendInt32Element(nil, "0", 123)) + doc = appendArrayElement(doc, "c", bsoncore.AppendInt64Element(nil, "0", 456)) + doc = appendArrayElement(doc, "d", bsoncore.AppendInt32Element(nil, "0", 789)) + doc = appendArrayElement(doc, "e", bsoncore.AppendInt64Element(nil, "0", 101112)) + doc = appendArrayElement(doc, "f", bsoncore.AppendDoubleElement(nil, "0", 3.14159)) + doc = appendArrayElement(doc, "g", bsoncore.AppendStringElement(nil, "0", "Hello, world")) + doc = appendArrayElement(doc, "h", bsoncore.BuildDocumentElement(nil, "0", bsoncore.AppendStringElement(nil, "foo", "bar"))) + doc = appendArrayElement(doc, "i", bsoncore.AppendBinaryElement(nil, "0", 0x00, []byte{0x01, 0x02, 0x03})) + doc = appendArrayElement(doc, "k", + appendArrayElement(nil, "0", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "baz"), "1", "qux")), + ) + doc = appendArrayElement(doc, "l", bsoncore.BuildDocumentElement(nil, "0", bsoncore.AppendStringElement(nil, "m", "foobar"))) + doc = appendArrayElement(doc, "n", + appendArrayElement(nil, "0", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "foo"), "1", "bar")), + ) + doc = appendArrayElement(doc, "r", + bsoncore.AppendObjectIDElement( + bsoncore.AppendObjectIDElement( + bsoncore.AppendObjectIDElement(nil, + "0", oids[0]), + "1", oids[1]), + "2", oids[2]), + ) + doc = bsoncore.AppendNullElement(doc, "t") + doc = bsoncore.AppendNullElement(doc, "w") + doc = appendArrayElement(doc, "x", nil) + doc = appendArrayElement(doc, "y", bsoncore.BuildDocumentElement(nil, "0", nil)) + doc = appendArrayElement(doc, "z", + bsoncore.AppendDateTimeElement( + bsoncore.AppendDateTimeElement( + nil, "0", now.UnixNano()/int64(time.Millisecond)), + "1", now.UnixNano()/int64(time.Millisecond)), + ) + doc = appendArrayElement(doc, "aa", bsoncore.AppendDoubleElement(bsoncore.AppendInt64Element(nil, "0", 5), "1", 10.10)) + doc = appendArrayElement(doc, "ab", bsoncore.AppendStringElement(nil, "0", murl.String())) + doc = appendArrayElement(doc, "ac", bsoncore.AppendDecimal128Element(nil, "0", decimal128)) + doc = appendArrayElement(doc, "ad", + bsoncore.AppendDateTimeElement( + bsoncore.AppendDateTimeElement(nil, "0", now.UnixNano()/int64(time.Millisecond)), + "1", now.UnixNano()/int64(time.Millisecond)), + ) + doc = appendArrayElement(doc, "ae", + bsoncore.AppendStringElement(bsoncore.AppendStringElement(nil, "0", "hello"), "1", "world"), + ) + doc = appendArrayElement(doc, "af", + bsoncore.AppendDocumentElement( + bsoncore.AppendDocumentElement(nil, "0", + bsoncore.BuildDocument(nil, bsoncore.AppendStringElement(nil, "hello", "world!")), + ), "1", + bsoncore.BuildDocument(nil, bsoncore.AppendStringElement(nil, "foo", "bar")), + ), + ) + doc = appendArrayElement(doc, "ag", + bsoncore.AppendDocumentElement( + bsoncore.AppendDocumentElement(nil, "0", + bsoncore.BuildDocument(nil, bsoncore.AppendInt64Element(nil, "one", 1234567890)), + ), "1", + bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159)), + ), + ) + return doc + }(nil)), + nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := make(bsonrw.SliceWriter, 0, 512) + vw, err := bsonrw.NewBSONValueWriter(&b) + noerr(t, err) + reg := buildDefaultRegistry() + enc, err := reg.LookupEncoder(reflect.TypeOf(tc.value)) + noerr(t, err) + err = enc.EncodeValue(EncodeContext{Registry: reg}, vw, reflect.ValueOf(tc.value)) + if err != tc.err { + t.Errorf("Did not receive expected error. got %v; want %v", err, tc.err) + } + if diff := cmp.Diff([]byte(b), tc.b); diff != "" { + t.Errorf("Bytes written differ: (-got +want)\n%s", diff) + t.Errorf("Bytes\ngot: %v\nwant:%v\n", b, tc.b) + t.Errorf("Readers\ngot: %v\nwant:%v\n", bsoncore.Document(b), bsoncore.Document(tc.b)) + } + }) + } + }) + + t.Run("error path", func(t *testing.T) { + testCases := []struct { + name string + value interface{} + err error + }{ + { + "duplicate name struct", + struct { + A int64 + B int64 `bson:"a"` + }{ + A: 0, + B: 54321, + }, + fmt.Errorf("duplicated key a"), + }, + { + "inline map", + struct { + Foo map[string]string `bson:",inline"` + Baz string + }{ + Foo: map[string]string{"baz": "bar"}, + Baz: "hi", + }, + fmt.Errorf("Key baz of inlined map conflicts with a struct field name"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := make(bsonrw.SliceWriter, 0, 512) + vw, err := bsonrw.NewBSONValueWriter(&b) + noerr(t, err) + reg := buildDefaultRegistry() + enc, err := reg.LookupEncoder(reflect.TypeOf(tc.value)) + noerr(t, err) + err = enc.EncodeValue(EncodeContext{Registry: reg}, vw, reflect.ValueOf(tc.value)) + if err == nil || !strings.Contains(err.Error(), tc.err.Error()) { + t.Errorf("Did not receive expected error. got %v; want %v", err, tc.err) + } + }) + } + }) + + t.Run("EmptyInterfaceEncodeValue/nil", func(t *testing.T) { + val := reflect.New(tEmpty).Elem() + llvrw := new(bsonrwtest.ValueReaderWriter) + err := dve.EmptyInterfaceEncodeValue(EncodeContext{Registry: NewRegistryBuilder().Build()}, llvrw, val) + noerr(t, err) + if llvrw.Invoked != bsonrwtest.WriteNull { + t.Errorf("Incorrect method called. got %v; want %v", llvrw.Invoked, bsonrwtest.WriteNull) + } + }) + + t.Run("EmptyInterfaceEncodeValue/LookupEncoder error", func(t *testing.T) { + val := reflect.New(tEmpty).Elem() + val.Set(reflect.ValueOf(int64(1234567890))) + llvrw := new(bsonrwtest.ValueReaderWriter) + got := dve.EmptyInterfaceEncodeValue(EncodeContext{Registry: NewRegistryBuilder().Build()}, llvrw, val) + want := ErrNoEncoder{Type: tInt64} + if !compareErrors(got, want) { + t.Errorf("Did not receive expected error. got %v; want %v", got, want) + } + }) +} + +type testValueMarshaler struct { + t bsontype.Type + buf []byte + err error +} + +func (tvm testValueMarshaler) MarshalBSONValue() (bsontype.Type, []byte, error) { + return tvm.t, tvm.buf, tvm.err +} + +type testValueMarshalPtr struct { + t bsontype.Type + buf []byte + err error +} + +func (tvm *testValueMarshalPtr) MarshalBSONValue() (bsontype.Type, []byte, error) { + return tvm.t, tvm.buf, tvm.err +} + +type testMarshaler struct { + buf []byte + err error +} + +func (tvm testMarshaler) MarshalBSON() ([]byte, error) { + return tvm.buf, tvm.err +} + +type testMarshalPtr struct { + buf []byte + err error +} + +func (tvm *testMarshalPtr) MarshalBSON() ([]byte, error) { + return tvm.buf, tvm.err +} + +type testProxy struct { + ret interface{} + err error +} + +func (tp testProxy) ProxyBSON() (interface{}, error) { return tp.ret, tp.err } + +type testProxyPtr struct { + ret interface{} + err error +} + +func (tp *testProxyPtr) ProxyBSON() (interface{}, error) { return tp.ret, tp.err } diff --git a/mongo/bson/bsoncodec/doc.go b/mongo/bson/bsoncodec/doc.go new file mode 100644 index 0000000..5f903eb --- /dev/null +++ b/mongo/bson/bsoncodec/doc.go @@ -0,0 +1,90 @@ +// Copyright (C) MongoDB, Inc. 2022-present. +// +// 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 + +// Package bsoncodec provides a system for encoding values to BSON representations and decoding +// values from BSON representations. This package considers both binary BSON and ExtendedJSON as +// BSON representations. The types in this package enable a flexible system for handling this +// encoding and decoding. +// +// The codec system is composed of two parts: +// +// 1) ValueEncoders and ValueDecoders that handle encoding and decoding Go values to and from BSON +// representations. +// +// 2) A Registry that holds these ValueEncoders and ValueDecoders and provides methods for +// retrieving them. +// +// # ValueEncoders and ValueDecoders +// +// The ValueEncoder interface is implemented by types that can encode a provided Go type to BSON. +// The value to encode is provided as a reflect.Value and a bsonrw.ValueWriter is used within the +// EncodeValue method to actually create the BSON representation. For convenience, ValueEncoderFunc +// is provided to allow use of a function with the correct signature as a ValueEncoder. An +// EncodeContext instance is provided to allow implementations to lookup further ValueEncoders and +// to provide configuration information. +// +// The ValueDecoder interface is the inverse of the ValueEncoder. Implementations should ensure that +// the value they receive is settable. Similar to ValueEncoderFunc, ValueDecoderFunc is provided to +// allow the use of a function with the correct signature as a ValueDecoder. A DecodeContext +// instance is provided and serves similar functionality to the EncodeContext. +// +// # Registry and RegistryBuilder +// +// A Registry is an immutable store for ValueEncoders, ValueDecoders, and a type map. See the Registry type +// documentation for examples of registering various custom encoders and decoders. A Registry can be constructed using a +// RegistryBuilder, which handles three main types of codecs: +// +// 1. Type encoders/decoders - These can be registered using the RegisterTypeEncoder and RegisterTypeDecoder methods. +// The registered codec will be invoked when encoding/decoding a value whose type matches the registered type exactly. +// If the registered type is an interface, the codec will be invoked when encoding or decoding values whose type is the +// interface, but not for values with concrete types that implement the interface. +// +// 2. Hook encoders/decoders - These can be registered using the RegisterHookEncoder and RegisterHookDecoder methods. +// These methods only accept interface types and the registered codecs will be invoked when encoding or decoding values +// whose types implement the interface. An example of a hook defined by the driver is bson.Marshaler. The driver will +// call the MarshalBSON method for any value whose type implements bson.Marshaler, regardless of the value's concrete +// type. +// +// 3. Type map entries - This can be used to associate a BSON type with a Go type. These type associations are used when +// decoding into a bson.D/bson.M or a struct field of type interface{}. For example, by default, BSON int32 and int64 +// values decode as Go int32 and int64 instances, respectively, when decoding into a bson.D. The following code would +// change the behavior so these values decode as Go int instances instead: +// +// intType := reflect.TypeOf(int(0)) +// registryBuilder.RegisterTypeMapEntry(bsontype.Int32, intType).RegisterTypeMapEntry(bsontype.Int64, intType) +// +// 4. Kind encoder/decoders - These can be registered using the RegisterDefaultEncoder and RegisterDefaultDecoder +// methods. The registered codec will be invoked when encoding or decoding values whose reflect.Kind matches the +// registered reflect.Kind as long as the value's type doesn't match a registered type or hook encoder/decoder first. +// These methods should be used to change the behavior for all values for a specific kind. +// +// # Registry Lookup Procedure +// +// When looking up an encoder in a Registry, the precedence rules are as follows: +// +// 1. A type encoder registered for the exact type of the value. +// +// 2. A hook encoder registered for an interface that is implemented by the value or by a pointer to the value. If the +// value matches multiple hooks (e.g. the type implements bsoncodec.Marshaler and bsoncodec.ValueMarshaler), the first +// one registered will be selected. Note that registries constructed using bson.NewRegistryBuilder have driver-defined +// hooks registered for the bsoncodec.Marshaler, bsoncodec.ValueMarshaler, and bsoncodec.Proxy interfaces, so those +// will take precedence over any new hooks. +// +// 3. A kind encoder registered for the value's kind. +// +// If all of these lookups fail to find an encoder, an error of type ErrNoEncoder is returned. The same precedence +// rules apply for decoders, with the exception that an error of type ErrNoDecoder will be returned if no decoder is +// found. +// +// # DefaultValueEncoders and DefaultValueDecoders +// +// The DefaultValueEncoders and DefaultValueDecoders types provide a full set of ValueEncoders and +// ValueDecoders for handling a wide range of Go types, including all of the types within the +// primitive package. To make registering these codecs easier, a helper method on each type is +// provided. For the DefaultValueEncoders type the method is called RegisterDefaultEncoders and for +// the DefaultValueDecoders type the method is called RegisterDefaultDecoders, this method also +// handles registering type map entries for each BSON type. +package bsoncodec diff --git a/mongo/bson/bsoncodec/empty_interface_codec.go b/mongo/bson/bsoncodec/empty_interface_codec.go new file mode 100644 index 0000000..eda417c --- /dev/null +++ b/mongo/bson/bsoncodec/empty_interface_codec.go @@ -0,0 +1,147 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// EmptyInterfaceCodec is the Codec used for interface{} values. +type EmptyInterfaceCodec struct { + DecodeBinaryAsSlice bool +} + +var ( + defaultEmptyInterfaceCodec = NewEmptyInterfaceCodec() + + _ ValueCodec = defaultEmptyInterfaceCodec + _ typeDecoder = defaultEmptyInterfaceCodec +) + +// NewEmptyInterfaceCodec returns a EmptyInterfaceCodec with options opts. +func NewEmptyInterfaceCodec(opts ...*bsonoptions.EmptyInterfaceCodecOptions) *EmptyInterfaceCodec { + interfaceOpt := bsonoptions.MergeEmptyInterfaceCodecOptions(opts...) + + codec := EmptyInterfaceCodec{} + if interfaceOpt.DecodeBinaryAsSlice != nil { + codec.DecodeBinaryAsSlice = *interfaceOpt.DecodeBinaryAsSlice + } + return &codec +} + +// EncodeValue is the ValueEncoderFunc for interface{}. +func (eic EmptyInterfaceCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tEmpty { + return ValueEncoderError{Name: "EmptyInterfaceEncodeValue", Types: []reflect.Type{tEmpty}, Received: val} + } + + if val.IsNil() { + return vw.WriteNull() + } + encoder, err := ec.LookupEncoder(val.Elem().Type()) + if err != nil { + return err + } + + return encoder.EncodeValue(ec, vw, val.Elem()) +} + +func (eic EmptyInterfaceCodec) getEmptyInterfaceDecodeType(dc DecodeContext, valueType bsontype.Type) (reflect.Type, error) { + isDocument := valueType == bsontype.Type(0) || valueType == bsontype.EmbeddedDocument + if isDocument { + if dc.defaultDocumentType != nil { + // If the bsontype is an embedded document and the DocumentType is set on the DecodeContext, then return + // that type. + return dc.defaultDocumentType, nil + } + if dc.Ancestor != nil { + // Using ancestor information rather than looking up the type map entry forces consistent decoding. + // If we're decoding into a bson.D, subdocuments should also be decoded as bson.D, even if a type map entry + // has been registered. + return dc.Ancestor, nil + } + } + + rtype, err := dc.LookupTypeMapEntry(valueType) + if err == nil { + return rtype, nil + } + + if isDocument { + // For documents, fallback to looking up a type map entry for bsontype.Type(0) or bsontype.EmbeddedDocument, + // depending on the original valueType. + var lookupType bsontype.Type + switch valueType { + case bsontype.Type(0): + lookupType = bsontype.EmbeddedDocument + case bsontype.EmbeddedDocument: + lookupType = bsontype.Type(0) + } + + rtype, err = dc.LookupTypeMapEntry(lookupType) + if err == nil { + return rtype, nil + } + } + + return nil, err +} + +func (eic EmptyInterfaceCodec) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tEmpty { + return emptyValue, ValueDecoderError{Name: "EmptyInterfaceDecodeValue", Types: []reflect.Type{tEmpty}, Received: reflect.Zero(t)} + } + + rtype, err := eic.getEmptyInterfaceDecodeType(dc, vr.Type()) + if err != nil { + switch vr.Type() { + case bsontype.Null: + return reflect.Zero(t), vr.ReadNull() + default: + return emptyValue, err + } + } + + decoder, err := dc.LookupDecoder(rtype) + if err != nil { + return emptyValue, err + } + + elem, err := decodeTypeOrValue(decoder, dc, vr, rtype) + if err != nil { + return emptyValue, err + } + + if eic.DecodeBinaryAsSlice && rtype == tBinary { + binElem := elem.Interface().(primitive.Binary) + if binElem.Subtype == bsontype.BinaryGeneric || binElem.Subtype == bsontype.BinaryBinaryOld { + elem = reflect.ValueOf(binElem.Data) + } + } + + return elem, nil +} + +// DecodeValue is the ValueDecoderFunc for interface{}. +func (eic EmptyInterfaceCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tEmpty { + return ValueDecoderError{Name: "EmptyInterfaceDecodeValue", Types: []reflect.Type{tEmpty}, Received: val} + } + + elem, err := eic.decodeType(dc, vr, val.Type()) + if err != nil { + return err + } + + val.Set(elem) + return nil +} diff --git a/mongo/bson/bsoncodec/map_codec.go b/mongo/bson/bsoncodec/map_codec.go new file mode 100644 index 0000000..e1fbef9 --- /dev/null +++ b/mongo/bson/bsoncodec/map_codec.go @@ -0,0 +1,309 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "encoding" + "fmt" + "reflect" + "strconv" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +var defaultMapCodec = NewMapCodec() + +// MapCodec is the Codec used for map values. +type MapCodec struct { + DecodeZerosMap bool + EncodeNilAsEmpty bool + EncodeKeysWithStringer bool +} + +var _ ValueCodec = &MapCodec{} + +// KeyMarshaler is the interface implemented by an object that can marshal itself into a string key. +// This applies to types used as map keys and is similar to encoding.TextMarshaler. +type KeyMarshaler interface { + MarshalKey() (key string, err error) +} + +// KeyUnmarshaler is the interface implemented by an object that can unmarshal a string representation +// of itself. This applies to types used as map keys and is similar to encoding.TextUnmarshaler. +// +// UnmarshalKey must be able to decode the form generated by MarshalKey. +// UnmarshalKey must copy the text if it wishes to retain the text +// after returning. +type KeyUnmarshaler interface { + UnmarshalKey(key string) error +} + +// NewMapCodec returns a MapCodec with options opts. +func NewMapCodec(opts ...*bsonoptions.MapCodecOptions) *MapCodec { + mapOpt := bsonoptions.MergeMapCodecOptions(opts...) + + codec := MapCodec{} + if mapOpt.DecodeZerosMap != nil { + codec.DecodeZerosMap = *mapOpt.DecodeZerosMap + } + if mapOpt.EncodeNilAsEmpty != nil { + codec.EncodeNilAsEmpty = *mapOpt.EncodeNilAsEmpty + } + if mapOpt.EncodeKeysWithStringer != nil { + codec.EncodeKeysWithStringer = *mapOpt.EncodeKeysWithStringer + } + return &codec +} + +// EncodeValue is the ValueEncoder for map[*]* types. +func (mc *MapCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Map { + return ValueEncoderError{Name: "MapEncodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: val} + } + + if val.IsNil() && !mc.EncodeNilAsEmpty { + // If we have a nil map but we can't WriteNull, that means we're probably trying to encode + // to a TopLevel document. We can't currently tell if this is what actually happened, but if + // there's a deeper underlying problem, the error will also be returned from WriteDocument, + // so just continue. The operations on a map reflection value are valid, so we can call + // MapKeys within mapEncodeValue without a problem. + err := vw.WriteNull() + if err == nil { + return nil + } + } + + dw, err := vw.WriteDocument() + if err != nil { + return err + } + + return mc.mapEncodeValue(ec, dw, val, nil) +} + +// mapEncodeValue handles encoding of the values of a map. The collisionFn returns +// true if the provided key exists, this is mainly used for inline maps in the +// struct codec. +func (mc *MapCodec) mapEncodeValue(ec EncodeContext, dw bsonrw.DocumentWriter, val reflect.Value, collisionFn func(string) bool) error { + + elemType := val.Type().Elem() + encoder, err := ec.LookupEncoder(elemType) + if err != nil && elemType.Kind() != reflect.Interface { + return err + } + + keys := val.MapKeys() + for _, key := range keys { + keyStr, err := mc.encodeKey(key) + if err != nil { + return err + } + + if collisionFn != nil && collisionFn(keyStr) { + return fmt.Errorf("Key %s of inlined map conflicts with a struct field name", key) + } + + currEncoder, currVal, lookupErr := defaultValueEncoders.lookupElementEncoder(ec, encoder, val.MapIndex(key)) + if lookupErr != nil && lookupErr != errInvalidValue { + return lookupErr + } + + vw, err := dw.WriteDocumentElement(keyStr) + if err != nil { + return err + } + + if lookupErr == errInvalidValue { + err = vw.WriteNull() + if err != nil { + return err + } + continue + } + + err = currEncoder.EncodeValue(ec, vw, currVal) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() +} + +// DecodeValue is the ValueDecoder for map[string/decimal]* types. +func (mc *MapCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if val.Kind() != reflect.Map || (!val.CanSet() && val.IsNil()) { + return ValueDecoderError{Name: "MapDecodeValue", Kinds: []reflect.Kind{reflect.Map}, Received: val} + } + + switch vrType := vr.Type(); vrType { + case bsontype.Type(0), bsontype.EmbeddedDocument: + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + case bsontype.Undefined: + val.Set(reflect.Zero(val.Type())) + return vr.ReadUndefined() + default: + return fmt.Errorf("cannot decode %v into a %s", vrType, val.Type()) + } + + dr, err := vr.ReadDocument() + if err != nil { + return err + } + + if val.IsNil() { + val.Set(reflect.MakeMap(val.Type())) + } + + if val.Len() > 0 && mc.DecodeZerosMap { + clearMap(val) + } + + eType := val.Type().Elem() + decoder, err := dc.LookupDecoder(eType) + if err != nil { + return err + } + eTypeDecoder, _ := decoder.(typeDecoder) + + if eType == tEmpty { + dc.Ancestor = val.Type() + } + + keyType := val.Type().Key() + + for { + key, vr, err := dr.ReadElement() + if err == bsonrw.ErrEOD { + break + } + if err != nil { + return err + } + + k, err := mc.decodeKey(key, keyType) + if err != nil { + return err + } + + elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, eType, true) + if err != nil { + return newDecodeError(key, err) + } + + val.SetMapIndex(k, elem) + } + return nil +} + +func clearMap(m reflect.Value) { + var none reflect.Value + for _, k := range m.MapKeys() { + m.SetMapIndex(k, none) + } +} + +func (mc *MapCodec) encodeKey(val reflect.Value) (string, error) { + if mc.EncodeKeysWithStringer { + return fmt.Sprint(val), nil + } + + // keys of any string type are used directly + if val.Kind() == reflect.String { + return val.String(), nil + } + // KeyMarshalers are marshaled + if km, ok := val.Interface().(KeyMarshaler); ok { + if val.Kind() == reflect.Ptr && val.IsNil() { + return "", nil + } + buf, err := km.MarshalKey() + if err == nil { + return buf, nil + } + return "", err + } + // keys implement encoding.TextMarshaler are marshaled. + if km, ok := val.Interface().(encoding.TextMarshaler); ok { + if val.Kind() == reflect.Ptr && val.IsNil() { + return "", nil + } + + buf, err := km.MarshalText() + if err != nil { + return "", err + } + + return string(buf), nil + } + + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(val.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(val.Uint(), 10), nil + } + return "", fmt.Errorf("unsupported key type: %v", val.Type()) +} + +var keyUnmarshalerType = reflect.TypeOf((*KeyUnmarshaler)(nil)).Elem() +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + +func (mc *MapCodec) decodeKey(key string, keyType reflect.Type) (reflect.Value, error) { + keyVal := reflect.ValueOf(key) + var err error + switch { + // First, if EncodeKeysWithStringer is not enabled, try to decode withKeyUnmarshaler + case !mc.EncodeKeysWithStringer && reflect.PtrTo(keyType).Implements(keyUnmarshalerType): + keyVal = reflect.New(keyType) + v := keyVal.Interface().(KeyUnmarshaler) + err = v.UnmarshalKey(key) + keyVal = keyVal.Elem() + // Try to decode encoding.TextUnmarshalers. + case reflect.PtrTo(keyType).Implements(textUnmarshalerType): + keyVal = reflect.New(keyType) + v := keyVal.Interface().(encoding.TextUnmarshaler) + err = v.UnmarshalText([]byte(key)) + keyVal = keyVal.Elem() + // Otherwise, go to type specific behavior + default: + switch keyType.Kind() { + case reflect.String: + keyVal = reflect.ValueOf(key).Convert(keyType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, parseErr := strconv.ParseInt(key, 10, 64) + if parseErr != nil || reflect.Zero(keyType).OverflowInt(n) { + err = fmt.Errorf("failed to unmarshal number key %v", key) + } + keyVal = reflect.ValueOf(n).Convert(keyType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, parseErr := strconv.ParseUint(key, 10, 64) + if parseErr != nil || reflect.Zero(keyType).OverflowUint(n) { + err = fmt.Errorf("failed to unmarshal number key %v", key) + break + } + keyVal = reflect.ValueOf(n).Convert(keyType) + case reflect.Float32, reflect.Float64: + if mc.EncodeKeysWithStringer { + parsed, err := strconv.ParseFloat(key, 64) + if err != nil { + return keyVal, fmt.Errorf("Map key is defined to be a decimal type (%v) but got error %v", keyType.Kind(), err) + } + keyVal = reflect.ValueOf(parsed) + break + } + fallthrough + default: + return keyVal, fmt.Errorf("unsupported key type: %v", keyType) + } + } + return keyVal, err +} diff --git a/mongo/bson/bsoncodec/mode.go b/mongo/bson/bsoncodec/mode.go new file mode 100644 index 0000000..fbd9f0a --- /dev/null +++ b/mongo/bson/bsoncodec/mode.go @@ -0,0 +1,65 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import "fmt" + +type mode int + +const ( + _ mode = iota + mTopLevel + mDocument + mArray + mValue + mElement + mCodeWithScope + mSpacer +) + +func (m mode) String() string { + var str string + + switch m { + case mTopLevel: + str = "TopLevel" + case mDocument: + str = "DocumentMode" + case mArray: + str = "ArrayMode" + case mValue: + str = "ValueMode" + case mElement: + str = "ElementMode" + case mCodeWithScope: + str = "CodeWithScopeMode" + case mSpacer: + str = "CodeWithScopeSpacerFrame" + default: + str = "UnknownMode" + } + + return str +} + +// TransitionError is an error returned when an invalid progressing a +// ValueReader or ValueWriter state machine occurs. +type TransitionError struct { + parent mode + current mode + destination mode +} + +func (te TransitionError) Error() string { + if te.destination == mode(0) { + return fmt.Sprintf("invalid state transition: cannot read/write value while in %s", te.current) + } + if te.parent == mode(0) { + return fmt.Sprintf("invalid state transition: %s -> %s", te.current, te.destination) + } + return fmt.Sprintf("invalid state transition: %s -> %s; parent %s", te.current, te.destination, te.parent) +} diff --git a/mongo/bson/bsoncodec/pointer_codec.go b/mongo/bson/bsoncodec/pointer_codec.go new file mode 100644 index 0000000..616a3e7 --- /dev/null +++ b/mongo/bson/bsoncodec/pointer_codec.go @@ -0,0 +1,109 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "sync" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +var _ ValueEncoder = &PointerCodec{} +var _ ValueDecoder = &PointerCodec{} + +// PointerCodec is the Codec used for pointers. +type PointerCodec struct { + ecache map[reflect.Type]ValueEncoder + dcache map[reflect.Type]ValueDecoder + l sync.RWMutex +} + +// NewPointerCodec returns a PointerCodec that has been initialized. +func NewPointerCodec() *PointerCodec { + return &PointerCodec{ + ecache: make(map[reflect.Type]ValueEncoder), + dcache: make(map[reflect.Type]ValueDecoder), + } +} + +// EncodeValue handles encoding a pointer by either encoding it to BSON Null if the pointer is nil +// or looking up an encoder for the type of value the pointer points to. +func (pc *PointerCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if val.Kind() != reflect.Ptr { + if !val.IsValid() { + return vw.WriteNull() + } + return ValueEncoderError{Name: "PointerCodec.EncodeValue", Kinds: []reflect.Kind{reflect.Ptr}, Received: val} + } + + if val.IsNil() { + return vw.WriteNull() + } + + pc.l.RLock() + enc, ok := pc.ecache[val.Type()] + pc.l.RUnlock() + if ok { + if enc == nil { + return ErrNoEncoder{Type: val.Type()} + } + return enc.EncodeValue(ec, vw, val.Elem()) + } + + enc, err := ec.LookupEncoder(val.Type().Elem()) + pc.l.Lock() + pc.ecache[val.Type()] = enc + pc.l.Unlock() + if err != nil { + return err + } + + return enc.EncodeValue(ec, vw, val.Elem()) +} + +// DecodeValue handles decoding a pointer by looking up a decoder for the type it points to and +// using that to decode. If the BSON value is Null, this method will set the pointer to nil. +func (pc *PointerCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.Ptr { + return ValueDecoderError{Name: "PointerCodec.DecodeValue", Kinds: []reflect.Kind{reflect.Ptr}, Received: val} + } + + if vr.Type() == bsontype.Null { + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + } + if vr.Type() == bsontype.Undefined { + val.Set(reflect.Zero(val.Type())) + return vr.ReadUndefined() + } + + if val.IsNil() { + val.Set(reflect.New(val.Type().Elem())) + } + + pc.l.RLock() + dec, ok := pc.dcache[val.Type()] + pc.l.RUnlock() + if ok { + if dec == nil { + return ErrNoDecoder{Type: val.Type()} + } + return dec.DecodeValue(dc, vr, val.Elem()) + } + + dec, err := dc.LookupDecoder(val.Type().Elem()) + pc.l.Lock() + pc.dcache[val.Type()] = dec + pc.l.Unlock() + if err != nil { + return err + } + + return dec.DecodeValue(dc, vr, val.Elem()) +} diff --git a/mongo/bson/bsoncodec/proxy.go b/mongo/bson/bsoncodec/proxy.go new file mode 100644 index 0000000..4cf2b01 --- /dev/null +++ b/mongo/bson/bsoncodec/proxy.go @@ -0,0 +1,14 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +// Proxy is an interface implemented by types that cannot themselves be directly encoded. Types +// that implement this interface with have ProxyBSON called during the encoding process and that +// value will be encoded in place for the implementer. +type Proxy interface { + ProxyBSON() (interface{}, error) +} diff --git a/mongo/bson/bsoncodec/registry.go b/mongo/bson/bsoncodec/registry.go new file mode 100644 index 0000000..8064402 --- /dev/null +++ b/mongo/bson/bsoncodec/registry.go @@ -0,0 +1,469 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "errors" + "fmt" + "reflect" + "sync" + + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// ErrNilType is returned when nil is passed to either LookupEncoder or LookupDecoder. +var ErrNilType = errors.New("cannot perform a decoder lookup on ") + +// ErrNotPointer is returned when a non-pointer type is provided to LookupDecoder. +var ErrNotPointer = errors.New("non-pointer provided to LookupDecoder") + +// ErrNoEncoder is returned when there wasn't an encoder available for a type. +type ErrNoEncoder struct { + Type reflect.Type +} + +func (ene ErrNoEncoder) Error() string { + if ene.Type == nil { + return "no encoder found for " + } + return "no encoder found for " + ene.Type.String() +} + +// ErrNoDecoder is returned when there wasn't a decoder available for a type. +type ErrNoDecoder struct { + Type reflect.Type +} + +func (end ErrNoDecoder) Error() string { + return "no decoder found for " + end.Type.String() +} + +// ErrNoTypeMapEntry is returned when there wasn't a type available for the provided BSON type. +type ErrNoTypeMapEntry struct { + Type bsontype.Type +} + +func (entme ErrNoTypeMapEntry) Error() string { + return "no type map entry found for " + entme.Type.String() +} + +// ErrNotInterface is returned when the provided type is not an interface. +var ErrNotInterface = errors.New("The provided type is not an interface") + +// A RegistryBuilder is used to build a Registry. This type is not goroutine +// safe. +type RegistryBuilder struct { + typeEncoders map[reflect.Type]ValueEncoder + interfaceEncoders []interfaceValueEncoder + kindEncoders map[reflect.Kind]ValueEncoder + + typeDecoders map[reflect.Type]ValueDecoder + interfaceDecoders []interfaceValueDecoder + kindDecoders map[reflect.Kind]ValueDecoder + + typeMap map[bsontype.Type]reflect.Type +} + +// A Registry is used to store and retrieve codecs for types and interfaces. This type is the main +// typed passed around and Encoders and Decoders are constructed from it. +type Registry struct { + typeEncoders map[reflect.Type]ValueEncoder + typeDecoders map[reflect.Type]ValueDecoder + + interfaceEncoders []interfaceValueEncoder + interfaceDecoders []interfaceValueDecoder + + kindEncoders map[reflect.Kind]ValueEncoder + kindDecoders map[reflect.Kind]ValueDecoder + + typeMap map[bsontype.Type]reflect.Type + + mu sync.RWMutex +} + +// NewRegistryBuilder creates a new empty RegistryBuilder. +func NewRegistryBuilder() *RegistryBuilder { + return &RegistryBuilder{ + typeEncoders: make(map[reflect.Type]ValueEncoder), + typeDecoders: make(map[reflect.Type]ValueDecoder), + + interfaceEncoders: make([]interfaceValueEncoder, 0), + interfaceDecoders: make([]interfaceValueDecoder, 0), + + kindEncoders: make(map[reflect.Kind]ValueEncoder), + kindDecoders: make(map[reflect.Kind]ValueDecoder), + + typeMap: make(map[bsontype.Type]reflect.Type), + } +} + +func buildDefaultRegistry() *Registry { + rb := NewRegistryBuilder() + defaultValueEncoders.RegisterDefaultEncoders(rb) + defaultValueDecoders.RegisterDefaultDecoders(rb) + return rb.Build() +} + +// RegisterCodec will register the provided ValueCodec for the provided type. +func (rb *RegistryBuilder) RegisterCodec(t reflect.Type, codec ValueCodec) *RegistryBuilder { + rb.RegisterTypeEncoder(t, codec) + rb.RegisterTypeDecoder(t, codec) + return rb +} + +// RegisterTypeEncoder will register the provided ValueEncoder for the provided type. +// +// The type will be used directly, so an encoder can be registered for a type and a different encoder can be registered +// for a pointer to that type. +// +// If the given type is an interface, the encoder will be called when marshalling a type that is that interface. It +// will not be called when marshalling a non-interface type that implements the interface. +func (rb *RegistryBuilder) RegisterTypeEncoder(t reflect.Type, enc ValueEncoder) *RegistryBuilder { + rb.typeEncoders[t] = enc + return rb +} + +// RegisterHookEncoder will register an encoder for the provided interface type t. This encoder will be called when +// marshalling a type if the type implements t or a pointer to the type implements t. If the provided type is not +// an interface (i.e. t.Kind() != reflect.Interface), this method will panic. +func (rb *RegistryBuilder) RegisterHookEncoder(t reflect.Type, enc ValueEncoder) *RegistryBuilder { + if t.Kind() != reflect.Interface { + panicStr := fmt.Sprintf("RegisterHookEncoder expects a type with kind reflect.Interface, "+ + "got type %s with kind %s", t, t.Kind()) + panic(panicStr) + } + + for idx, encoder := range rb.interfaceEncoders { + if encoder.i == t { + rb.interfaceEncoders[idx].ve = enc + return rb + } + } + + rb.interfaceEncoders = append(rb.interfaceEncoders, interfaceValueEncoder{i: t, ve: enc}) + return rb +} + +// RegisterTypeDecoder will register the provided ValueDecoder for the provided type. +// +// The type will be used directly, so a decoder can be registered for a type and a different decoder can be registered +// for a pointer to that type. +// +// If the given type is an interface, the decoder will be called when unmarshalling into a type that is that interface. +// It will not be called when unmarshalling into a non-interface type that implements the interface. +func (rb *RegistryBuilder) RegisterTypeDecoder(t reflect.Type, dec ValueDecoder) *RegistryBuilder { + rb.typeDecoders[t] = dec + return rb +} + +// RegisterHookDecoder will register an decoder for the provided interface type t. This decoder will be called when +// unmarshalling into a type if the type implements t or a pointer to the type implements t. If the provided type is not +// an interface (i.e. t.Kind() != reflect.Interface), this method will panic. +func (rb *RegistryBuilder) RegisterHookDecoder(t reflect.Type, dec ValueDecoder) *RegistryBuilder { + if t.Kind() != reflect.Interface { + panicStr := fmt.Sprintf("RegisterHookDecoder expects a type with kind reflect.Interface, "+ + "got type %s with kind %s", t, t.Kind()) + panic(panicStr) + } + + for idx, decoder := range rb.interfaceDecoders { + if decoder.i == t { + rb.interfaceDecoders[idx].vd = dec + return rb + } + } + + rb.interfaceDecoders = append(rb.interfaceDecoders, interfaceValueDecoder{i: t, vd: dec}) + return rb +} + +// RegisterEncoder registers the provided type and encoder pair. +// +// Deprecated: Use RegisterTypeEncoder or RegisterHookEncoder instead. +func (rb *RegistryBuilder) RegisterEncoder(t reflect.Type, enc ValueEncoder) *RegistryBuilder { + if t == tEmpty { + rb.typeEncoders[t] = enc + return rb + } + switch t.Kind() { + case reflect.Interface: + for idx, ir := range rb.interfaceEncoders { + if ir.i == t { + rb.interfaceEncoders[idx].ve = enc + return rb + } + } + + rb.interfaceEncoders = append(rb.interfaceEncoders, interfaceValueEncoder{i: t, ve: enc}) + default: + rb.typeEncoders[t] = enc + } + return rb +} + +// RegisterDecoder registers the provided type and decoder pair. +// +// Deprecated: Use RegisterTypeDecoder or RegisterHookDecoder instead. +func (rb *RegistryBuilder) RegisterDecoder(t reflect.Type, dec ValueDecoder) *RegistryBuilder { + if t == nil { + rb.typeDecoders[nil] = dec + return rb + } + if t == tEmpty { + rb.typeDecoders[t] = dec + return rb + } + switch t.Kind() { + case reflect.Interface: + for idx, ir := range rb.interfaceDecoders { + if ir.i == t { + rb.interfaceDecoders[idx].vd = dec + return rb + } + } + + rb.interfaceDecoders = append(rb.interfaceDecoders, interfaceValueDecoder{i: t, vd: dec}) + default: + rb.typeDecoders[t] = dec + } + return rb +} + +// RegisterDefaultEncoder will registr the provided ValueEncoder to the provided +// kind. +func (rb *RegistryBuilder) RegisterDefaultEncoder(kind reflect.Kind, enc ValueEncoder) *RegistryBuilder { + rb.kindEncoders[kind] = enc + return rb +} + +// RegisterDefaultDecoder will register the provided ValueDecoder to the +// provided kind. +func (rb *RegistryBuilder) RegisterDefaultDecoder(kind reflect.Kind, dec ValueDecoder) *RegistryBuilder { + rb.kindDecoders[kind] = dec + return rb +} + +// RegisterTypeMapEntry will register the provided type to the BSON type. The primary usage for this +// mapping is decoding situations where an empty interface is used and a default type needs to be +// created and decoded into. +// +// By default, BSON documents will decode into interface{} values as bson.D. To change the default type for BSON +// documents, a type map entry for bsontype.EmbeddedDocument should be registered. For example, to force BSON documents +// to decode to bson.Raw, use the following code: +// +// rb.RegisterTypeMapEntry(bsontype.EmbeddedDocument, reflect.TypeOf(bson.Raw{})) +func (rb *RegistryBuilder) RegisterTypeMapEntry(bt bsontype.Type, rt reflect.Type) *RegistryBuilder { + rb.typeMap[bt] = rt + return rb +} + +// Build creates a Registry from the current state of this RegistryBuilder. +func (rb *RegistryBuilder) Build() *Registry { + registry := new(Registry) + + registry.typeEncoders = make(map[reflect.Type]ValueEncoder) + for t, enc := range rb.typeEncoders { + registry.typeEncoders[t] = enc + } + + registry.typeDecoders = make(map[reflect.Type]ValueDecoder) + for t, dec := range rb.typeDecoders { + registry.typeDecoders[t] = dec + } + + registry.interfaceEncoders = make([]interfaceValueEncoder, len(rb.interfaceEncoders)) + copy(registry.interfaceEncoders, rb.interfaceEncoders) + + registry.interfaceDecoders = make([]interfaceValueDecoder, len(rb.interfaceDecoders)) + copy(registry.interfaceDecoders, rb.interfaceDecoders) + + registry.kindEncoders = make(map[reflect.Kind]ValueEncoder) + for kind, enc := range rb.kindEncoders { + registry.kindEncoders[kind] = enc + } + + registry.kindDecoders = make(map[reflect.Kind]ValueDecoder) + for kind, dec := range rb.kindDecoders { + registry.kindDecoders[kind] = dec + } + + registry.typeMap = make(map[bsontype.Type]reflect.Type) + for bt, rt := range rb.typeMap { + registry.typeMap[bt] = rt + } + + return registry +} + +// LookupEncoder inspects the registry for an encoder for the given type. The lookup precedence works as follows: +// +// 1. An encoder registered for the exact type. If the given type represents an interface, an encoder registered using +// RegisterTypeEncoder for the interface will be selected. +// +// 2. An encoder registered using RegisterHookEncoder for an interface implemented by the type or by a pointer to the +// type. +// +// 3. An encoder registered for the reflect.Kind of the value. +// +// If no encoder is found, an error of type ErrNoEncoder is returned. +func (r *Registry) LookupEncoder(t reflect.Type) (ValueEncoder, error) { + encodererr := ErrNoEncoder{Type: t} + r.mu.RLock() + enc, found := r.lookupTypeEncoder(t) + r.mu.RUnlock() + if found { + if enc == nil { + return nil, ErrNoEncoder{Type: t} + } + return enc, nil + } + + enc, found = r.lookupInterfaceEncoder(t, true) + if found { + r.mu.Lock() + r.typeEncoders[t] = enc + r.mu.Unlock() + return enc, nil + } + + if t == nil { + r.mu.Lock() + r.typeEncoders[t] = nil + r.mu.Unlock() + return nil, encodererr + } + + enc, found = r.kindEncoders[t.Kind()] + if !found { + r.mu.Lock() + r.typeEncoders[t] = nil + r.mu.Unlock() + return nil, encodererr + } + + r.mu.Lock() + r.typeEncoders[t] = enc + r.mu.Unlock() + return enc, nil +} + +func (r *Registry) lookupTypeEncoder(t reflect.Type) (ValueEncoder, bool) { + enc, found := r.typeEncoders[t] + return enc, found +} + +func (r *Registry) lookupInterfaceEncoder(t reflect.Type, allowAddr bool) (ValueEncoder, bool) { + if t == nil { + return nil, false + } + for _, ienc := range r.interfaceEncoders { + if t.Implements(ienc.i) { + return ienc.ve, true + } + if allowAddr && t.Kind() != reflect.Ptr && reflect.PtrTo(t).Implements(ienc.i) { + // if *t implements an interface, this will catch if t implements an interface further ahead + // in interfaceEncoders + defaultEnc, found := r.lookupInterfaceEncoder(t, false) + if !found { + defaultEnc = r.kindEncoders[t.Kind()] + } + return newCondAddrEncoder(ienc.ve, defaultEnc), true + } + } + return nil, false +} + +// LookupDecoder inspects the registry for an decoder for the given type. The lookup precedence works as follows: +// +// 1. A decoder registered for the exact type. If the given type represents an interface, a decoder registered using +// RegisterTypeDecoder for the interface will be selected. +// +// 2. A decoder registered using RegisterHookDecoder for an interface implemented by the type or by a pointer to the +// type. +// +// 3. A decoder registered for the reflect.Kind of the value. +// +// If no decoder is found, an error of type ErrNoDecoder is returned. +func (r *Registry) LookupDecoder(t reflect.Type) (ValueDecoder, error) { + if t == nil { + return nil, ErrNilType + } + decodererr := ErrNoDecoder{Type: t} + r.mu.RLock() + dec, found := r.lookupTypeDecoder(t) + r.mu.RUnlock() + if found { + if dec == nil { + return nil, ErrNoDecoder{Type: t} + } + return dec, nil + } + + dec, found = r.lookupInterfaceDecoder(t, true) + if found { + r.mu.Lock() + r.typeDecoders[t] = dec + r.mu.Unlock() + return dec, nil + } + + dec, found = r.kindDecoders[t.Kind()] + if !found { + r.mu.Lock() + r.typeDecoders[t] = nil + r.mu.Unlock() + return nil, decodererr + } + + r.mu.Lock() + r.typeDecoders[t] = dec + r.mu.Unlock() + return dec, nil +} + +func (r *Registry) lookupTypeDecoder(t reflect.Type) (ValueDecoder, bool) { + dec, found := r.typeDecoders[t] + return dec, found +} + +func (r *Registry) lookupInterfaceDecoder(t reflect.Type, allowAddr bool) (ValueDecoder, bool) { + for _, idec := range r.interfaceDecoders { + if t.Implements(idec.i) { + return idec.vd, true + } + if allowAddr && t.Kind() != reflect.Ptr && reflect.PtrTo(t).Implements(idec.i) { + // if *t implements an interface, this will catch if t implements an interface further ahead + // in interfaceDecoders + defaultDec, found := r.lookupInterfaceDecoder(t, false) + if !found { + defaultDec = r.kindDecoders[t.Kind()] + } + return newCondAddrDecoder(idec.vd, defaultDec), true + } + } + return nil, false +} + +// LookupTypeMapEntry inspects the registry's type map for a Go type for the corresponding BSON +// type. If no type is found, ErrNoTypeMapEntry is returned. +func (r *Registry) LookupTypeMapEntry(bt bsontype.Type) (reflect.Type, error) { + t, ok := r.typeMap[bt] + if !ok || t == nil { + return nil, ErrNoTypeMapEntry{Type: bt} + } + return t, nil +} + +type interfaceValueEncoder struct { + i reflect.Type + ve ValueEncoder +} + +type interfaceValueDecoder struct { + i reflect.Type + vd ValueDecoder +} diff --git a/mongo/bson/bsoncodec/registry_examples_test.go b/mongo/bson/bsoncodec/registry_examples_test.go new file mode 100644 index 0000000..e8f35ef --- /dev/null +++ b/mongo/bson/bsoncodec/registry_examples_test.go @@ -0,0 +1,125 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec_test + +import ( + "fmt" + "math" + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsoncodec" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +func ExampleRegistry_customEncoder() { + // Write a custom encoder for an integer type that is multiplied by -1 when + // encoding. + + // To register the default encoders and decoders in addition to this custom + // one, use bson.NewRegistryBuilder instead. + rb := bsoncodec.NewRegistryBuilder() + type negatedInt int + + niType := reflect.TypeOf(negatedInt(0)) + encoder := func( + ec bsoncodec.EncodeContext, + vw bsonrw.ValueWriter, + val reflect.Value, + ) error { + // All encoder implementations should check that val is valid and is of + // the correct type before proceeding. + if !val.IsValid() || val.Type() != niType { + return bsoncodec.ValueEncoderError{ + Name: "negatedIntEncodeValue", + Types: []reflect.Type{niType}, + Received: val, + } + } + + // Negate val and encode as a BSON int32 if it can fit in 32 bits and a + // BSON int64 otherwise. + negatedVal := val.Int() * -1 + if math.MinInt32 <= negatedVal && negatedVal <= math.MaxInt32 { + return vw.WriteInt32(int32(negatedVal)) + } + return vw.WriteInt64(negatedVal) + } + + rb.RegisterTypeEncoder(niType, bsoncodec.ValueEncoderFunc(encoder)) +} + +func ExampleRegistry_customDecoder() { + // Write a custom decoder for a boolean type that can be stored in the + // database as a BSON boolean, int32, int64, double, or null. BSON int32, + // int64, and double values are considered "true" in this decoder if they + // are non-zero. BSON null values are always considered false. + + // To register the default encoders and decoders in addition to this custom + // one, use bson.NewRegistryBuilder instead. + rb := bsoncodec.NewRegistryBuilder() + type lenientBool bool + + decoder := func( + dc bsoncodec.DecodeContext, + vr bsonrw.ValueReader, + val reflect.Value, + ) error { + // All decoder implementations should check that val is valid, settable, + // and is of the correct kind before proceeding. + if !val.IsValid() || !val.CanSet() || val.Kind() != reflect.Bool { + return bsoncodec.ValueDecoderError{ + Name: "lenientBoolDecodeValue", + Kinds: []reflect.Kind{reflect.Bool}, + Received: val, + } + } + + var result bool + switch vr.Type() { + case bsontype.Boolean: + b, err := vr.ReadBoolean() + if err != nil { + return err + } + result = b + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return err + } + result = i32 != 0 + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return err + } + result = i64 != 0 + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return err + } + result = f64 != 0 + case bsontype.Null: + if err := vr.ReadNull(); err != nil { + return err + } + result = false + default: + return fmt.Errorf( + "received invalid BSON type to decode into lenientBool: %s", + vr.Type()) + } + + val.SetBool(result) + return nil + } + + lbType := reflect.TypeOf(lenientBool(true)) + rb.RegisterTypeDecoder(lbType, bsoncodec.ValueDecoderFunc(decoder)) +} diff --git a/mongo/bson/bsoncodec/registry_test.go b/mongo/bson/bsoncodec/registry_test.go new file mode 100644 index 0000000..7f7ed79 --- /dev/null +++ b/mongo/bson/bsoncodec/registry_test.go @@ -0,0 +1,452 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/internal/testutil/assert" +) + +func TestRegistry(t *testing.T) { + t.Run("Register", func(t *testing.T) { + fc1, fc2, fc3, fc4 := new(fakeCodec), new(fakeCodec), new(fakeCodec), new(fakeCodec) + t.Run("interface", func(t *testing.T) { + var t1f *testInterface1 + var t2f *testInterface2 + var t4f *testInterface4 + ips := []interfaceValueEncoder{ + {i: reflect.TypeOf(t1f).Elem(), ve: fc1}, + {i: reflect.TypeOf(t2f).Elem(), ve: fc2}, + {i: reflect.TypeOf(t1f).Elem(), ve: fc3}, + {i: reflect.TypeOf(t4f).Elem(), ve: fc4}, + } + want := []interfaceValueEncoder{ + {i: reflect.TypeOf(t1f).Elem(), ve: fc3}, + {i: reflect.TypeOf(t2f).Elem(), ve: fc2}, + {i: reflect.TypeOf(t4f).Elem(), ve: fc4}, + } + rb := NewRegistryBuilder() + for _, ip := range ips { + rb.RegisterHookEncoder(ip.i, ip.ve) + } + got := rb.interfaceEncoders + if !cmp.Equal(got, want, cmp.AllowUnexported(interfaceValueEncoder{}, fakeCodec{}), cmp.Comparer(typeComparer)) { + t.Errorf("The registered interfaces are not correct. got %v; want %v", got, want) + } + }) + t.Run("type", func(t *testing.T) { + ft1, ft2, ft4 := fakeType1{}, fakeType2{}, fakeType4{} + rb := NewRegistryBuilder(). + RegisterTypeEncoder(reflect.TypeOf(ft1), fc1). + RegisterTypeEncoder(reflect.TypeOf(ft2), fc2). + RegisterTypeEncoder(reflect.TypeOf(ft1), fc3). + RegisterTypeEncoder(reflect.TypeOf(ft4), fc4) + want := []struct { + t reflect.Type + c ValueEncoder + }{ + {reflect.TypeOf(ft1), fc3}, + {reflect.TypeOf(ft2), fc2}, + {reflect.TypeOf(ft4), fc4}, + } + got := rb.typeEncoders + for _, s := range want { + wantT, wantC := s.t, s.c + gotC, exists := got[wantT] + if !exists { + t.Errorf("Did not find type in the type registry: %v", wantT) + } + if !cmp.Equal(gotC, wantC, cmp.AllowUnexported(fakeCodec{})) { + t.Errorf("Codecs did not match. got %#v; want %#v", gotC, wantC) + } + } + }) + t.Run("kind", func(t *testing.T) { + k1, k2, k4 := reflect.Struct, reflect.Slice, reflect.Map + rb := NewRegistryBuilder(). + RegisterDefaultEncoder(k1, fc1). + RegisterDefaultEncoder(k2, fc2). + RegisterDefaultEncoder(k1, fc3). + RegisterDefaultEncoder(k4, fc4) + want := []struct { + k reflect.Kind + c ValueEncoder + }{ + {k1, fc3}, + {k2, fc2}, + {k4, fc4}, + } + got := rb.kindEncoders + for _, s := range want { + wantK, wantC := s.k, s.c + gotC, exists := got[wantK] + if !exists { + t.Errorf("Did not find kind in the kind registry: %v", wantK) + } + if !cmp.Equal(gotC, wantC, cmp.AllowUnexported(fakeCodec{})) { + t.Errorf("Codecs did not match. got %#v; want %#v", gotC, wantC) + } + } + }) + t.Run("RegisterDefault", func(t *testing.T) { + t.Run("MapCodec", func(t *testing.T) { + codec := fakeCodec{num: 1} + codec2 := fakeCodec{num: 2} + rb := NewRegistryBuilder() + rb.RegisterDefaultEncoder(reflect.Map, codec) + if rb.kindEncoders[reflect.Map] != codec { + t.Errorf("Did not properly set the map codec. got %v; want %v", rb.kindEncoders[reflect.Map], codec) + } + rb.RegisterDefaultEncoder(reflect.Map, codec2) + if rb.kindEncoders[reflect.Map] != codec2 { + t.Errorf("Did not properly set the map codec. got %v; want %v", rb.kindEncoders[reflect.Map], codec2) + } + }) + t.Run("StructCodec", func(t *testing.T) { + codec := fakeCodec{num: 1} + codec2 := fakeCodec{num: 2} + rb := NewRegistryBuilder() + rb.RegisterDefaultEncoder(reflect.Struct, codec) + if rb.kindEncoders[reflect.Struct] != codec { + t.Errorf("Did not properly set the struct codec. got %v; want %v", rb.kindEncoders[reflect.Struct], codec) + } + rb.RegisterDefaultEncoder(reflect.Struct, codec2) + if rb.kindEncoders[reflect.Struct] != codec2 { + t.Errorf("Did not properly set the struct codec. got %v; want %v", rb.kindEncoders[reflect.Struct], codec2) + } + }) + t.Run("SliceCodec", func(t *testing.T) { + codec := fakeCodec{num: 1} + codec2 := fakeCodec{num: 2} + rb := NewRegistryBuilder() + rb.RegisterDefaultEncoder(reflect.Slice, codec) + if rb.kindEncoders[reflect.Slice] != codec { + t.Errorf("Did not properly set the slice codec. got %v; want %v", rb.kindEncoders[reflect.Slice], codec) + } + rb.RegisterDefaultEncoder(reflect.Slice, codec2) + if rb.kindEncoders[reflect.Slice] != codec2 { + t.Errorf("Did not properly set the slice codec. got %v; want %v", rb.kindEncoders[reflect.Slice], codec2) + } + }) + t.Run("ArrayCodec", func(t *testing.T) { + codec := fakeCodec{num: 1} + codec2 := fakeCodec{num: 2} + rb := NewRegistryBuilder() + rb.RegisterDefaultEncoder(reflect.Array, codec) + if rb.kindEncoders[reflect.Array] != codec { + t.Errorf("Did not properly set the slice codec. got %v; want %v", rb.kindEncoders[reflect.Array], codec) + } + rb.RegisterDefaultEncoder(reflect.Array, codec2) + if rb.kindEncoders[reflect.Array] != codec2 { + t.Errorf("Did not properly set the slice codec. got %v; want %v", rb.kindEncoders[reflect.Array], codec2) + } + }) + }) + t.Run("Lookup", func(t *testing.T) { + type Codec interface { + ValueEncoder + ValueDecoder + } + + var arrinstance [12]int + arr := reflect.TypeOf(arrinstance) + slc := reflect.TypeOf(make([]int, 12)) + m := reflect.TypeOf(make(map[string]int)) + strct := reflect.TypeOf(struct{ Foo string }{}) + ft1 := reflect.PtrTo(reflect.TypeOf(fakeType1{})) + ft2 := reflect.TypeOf(fakeType2{}) + ft3 := reflect.TypeOf(fakeType5(func(string, string) string { return "fakeType5" })) + ti1 := reflect.TypeOf((*testInterface1)(nil)).Elem() + ti2 := reflect.TypeOf((*testInterface2)(nil)).Elem() + ti1Impl := reflect.TypeOf(testInterface1Impl{}) + ti2Impl := reflect.TypeOf(testInterface2Impl{}) + ti3 := reflect.TypeOf((*testInterface3)(nil)).Elem() + ti3Impl := reflect.TypeOf(testInterface3Impl{}) + ti3ImplPtr := reflect.TypeOf((*testInterface3Impl)(nil)) + fc1, fc2 := fakeCodec{num: 1}, fakeCodec{num: 2} + fsc, fslcc, fmc := new(fakeStructCodec), new(fakeSliceCodec), new(fakeMapCodec) + pc := NewPointerCodec() + + reg := NewRegistryBuilder(). + RegisterTypeEncoder(ft1, fc1). + RegisterTypeEncoder(ft2, fc2). + RegisterTypeEncoder(ti1, fc1). + RegisterDefaultEncoder(reflect.Struct, fsc). + RegisterDefaultEncoder(reflect.Slice, fslcc). + RegisterDefaultEncoder(reflect.Array, fslcc). + RegisterDefaultEncoder(reflect.Map, fmc). + RegisterDefaultEncoder(reflect.Ptr, pc). + RegisterTypeDecoder(ft1, fc1). + RegisterTypeDecoder(ft2, fc2). + RegisterTypeDecoder(ti1, fc1). // values whose exact type is testInterface1 will use fc1 encoder + RegisterDefaultDecoder(reflect.Struct, fsc). + RegisterDefaultDecoder(reflect.Slice, fslcc). + RegisterDefaultDecoder(reflect.Array, fslcc). + RegisterDefaultDecoder(reflect.Map, fmc). + RegisterDefaultDecoder(reflect.Ptr, pc). + RegisterHookEncoder(ti2, fc2). + RegisterHookDecoder(ti2, fc2). + RegisterHookEncoder(ti3, fc3). + RegisterHookDecoder(ti3, fc3). + Build() + + testCases := []struct { + name string + t reflect.Type + wantcodec Codec + wanterr error + testcache bool + }{ + { + "type registry (pointer)", + ft1, + fc1, + nil, + false, + }, + { + "type registry (non-pointer)", + ft2, + fc2, + nil, + false, + }, + { + // lookup an interface type and expect that the registered encoder is returned + "interface with type encoder", + ti1, + fc1, + nil, + true, + }, + { + // lookup a type that implements an interface and expect that the default struct codec is returned + "interface implementation with type encoder", + ti1Impl, + fsc, + nil, + false, + }, + { + // lookup an interface type and expect that the registered hook is returned + "interface with hook", + ti2, + fc2, + nil, + false, + }, + { + // lookup a type that implements an interface and expect that the registered hook is returned + "interface implementation with hook", + ti2Impl, + fc2, + nil, + false, + }, + { + // lookup a pointer to a type where the pointer implements an interface and expect that the + // registered hook is returned + "interface pointer to implementation with hook (pointer)", + ti3ImplPtr, + fc3, + nil, + false, + }, + { + "default struct codec (pointer)", + reflect.PtrTo(strct), + pc, + nil, + false, + }, + { + "default struct codec (non-pointer)", + strct, + fsc, + nil, + false, + }, + { + "default array codec", + arr, + fslcc, + nil, + false, + }, + { + "default slice codec", + slc, + fslcc, + nil, + false, + }, + { + "default map", + m, + fmc, + nil, + false, + }, + { + "map non-string key", + reflect.TypeOf(map[int]int{}), + fmc, + nil, + false, + }, + { + "No Codec Registered", + ft3, + nil, + ErrNoEncoder{Type: ft3}, + false, + }, + } + + allowunexported := cmp.AllowUnexported(fakeCodec{}, fakeStructCodec{}, fakeSliceCodec{}, fakeMapCodec{}) + comparepc := func(pc1, pc2 *PointerCodec) bool { return true } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("Encoder", func(t *testing.T) { + gotcodec, goterr := reg.LookupEncoder(tc.t) + if !cmp.Equal(goterr, tc.wanterr, cmp.Comparer(compareErrors)) { + t.Errorf("Errors did not match. got %v; want %v", goterr, tc.wanterr) + } + if !cmp.Equal(gotcodec, tc.wantcodec, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("Codecs did not match. got %v; want %v", gotcodec, tc.wantcodec) + } + }) + t.Run("Decoder", func(t *testing.T) { + var wanterr error + if ene, ok := tc.wanterr.(ErrNoEncoder); ok { + wanterr = ErrNoDecoder(ene) + } else { + wanterr = tc.wanterr + } + gotcodec, goterr := reg.LookupDecoder(tc.t) + if !cmp.Equal(goterr, wanterr, cmp.Comparer(compareErrors)) { + t.Errorf("Errors did not match. got %v; want %v", goterr, wanterr) + } + if !cmp.Equal(gotcodec, tc.wantcodec, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("Codecs did not match. got %v; want %v", gotcodec, tc.wantcodec) + t.Errorf("Codecs did not match. got %T; want %T", gotcodec, tc.wantcodec) + } + }) + }) + } + // lookup a type whose pointer implements an interface and expect that the registered hook is + // returned + t.Run("interface implementation with hook (pointer)", func(t *testing.T) { + t.Run("Encoder", func(t *testing.T) { + gotEnc, err := reg.LookupEncoder(ti3Impl) + assert.Nil(t, err, "LookupEncoder error: %v", err) + + cae, ok := gotEnc.(*condAddrEncoder) + assert.True(t, ok, "Expected CondAddrEncoder, got %T", gotEnc) + if !cmp.Equal(cae.canAddrEnc, fc3, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("expected canAddrEnc %v, got %v", cae.canAddrEnc, fc3) + } + if !cmp.Equal(cae.elseEnc, fsc, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("expected elseEnc %v, got %v", cae.elseEnc, fsc) + } + }) + t.Run("Decoder", func(t *testing.T) { + gotDec, err := reg.LookupDecoder(ti3Impl) + assert.Nil(t, err, "LookupDecoder error: %v", err) + + cad, ok := gotDec.(*condAddrDecoder) + assert.True(t, ok, "Expected CondAddrDecoder, got %T", gotDec) + if !cmp.Equal(cad.canAddrDec, fc3, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("expected canAddrDec %v, got %v", cad.canAddrDec, fc3) + } + if !cmp.Equal(cad.elseDec, fsc, allowunexported, cmp.Comparer(comparepc)) { + t.Errorf("expected elseDec %v, got %v", cad.elseDec, fsc) + } + }) + }) + }) + }) + t.Run("Type Map", func(t *testing.T) { + reg := NewRegistryBuilder(). + RegisterTypeMapEntry(bsontype.String, reflect.TypeOf("")). + RegisterTypeMapEntry(bsontype.Int32, reflect.TypeOf(int(0))). + Build() + + var got, want reflect.Type + + want = reflect.TypeOf("") + got, err := reg.LookupTypeMapEntry(bsontype.String) + noerr(t, err) + if got != want { + t.Errorf("Did not get expected type. got %v; want %v", got, want) + } + + want = reflect.TypeOf(int(0)) + got, err = reg.LookupTypeMapEntry(bsontype.Int32) + noerr(t, err) + if got != want { + t.Errorf("Did not get expected type. got %v; want %v", got, want) + } + + want = nil + wanterr := ErrNoTypeMapEntry{Type: bsontype.ObjectID} + got, err = reg.LookupTypeMapEntry(bsontype.ObjectID) + if err != wanterr { + t.Errorf("Did not get expected error. got %v; want %v", err, wanterr) + } + if got != want { + t.Errorf("Did not get expected type. got %v; want %v", got, want) + } + }) +} + +type fakeType1 struct{} +type fakeType2 struct{} +type fakeType4 struct{} +type fakeType5 func(string, string) string +type fakeStructCodec struct{ fakeCodec } +type fakeSliceCodec struct{ fakeCodec } +type fakeMapCodec struct{ fakeCodec } + +type fakeCodec struct{ num int } + +func (fc fakeCodec) EncodeValue(EncodeContext, bsonrw.ValueWriter, reflect.Value) error { + return nil +} +func (fc fakeCodec) DecodeValue(DecodeContext, bsonrw.ValueReader, reflect.Value) error { + return nil +} + +type testInterface1 interface{ test1() } +type testInterface2 interface{ test2() } +type testInterface3 interface{ test3() } +type testInterface4 interface{ test4() } + +type testInterface1Impl struct{} + +var _ testInterface1 = testInterface1Impl{} + +func (testInterface1Impl) test1() {} + +type testInterface2Impl struct{} + +var _ testInterface2 = testInterface2Impl{} + +func (testInterface2Impl) test2() {} + +type testInterface3Impl struct{} + +var _ testInterface3 = (*testInterface3Impl)(nil) + +func (*testInterface3Impl) test3() {} + +func typeComparer(i1, i2 reflect.Type) bool { return i1 == i2 } diff --git a/mongo/bson/bsoncodec/slice_codec.go b/mongo/bson/bsoncodec/slice_codec.go new file mode 100644 index 0000000..3c1b6b8 --- /dev/null +++ b/mongo/bson/bsoncodec/slice_codec.go @@ -0,0 +1,199 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var defaultSliceCodec = NewSliceCodec() + +// SliceCodec is the Codec used for slice values. +type SliceCodec struct { + EncodeNilAsEmpty bool +} + +var _ ValueCodec = &MapCodec{} + +// NewSliceCodec returns a MapCodec with options opts. +func NewSliceCodec(opts ...*bsonoptions.SliceCodecOptions) *SliceCodec { + sliceOpt := bsonoptions.MergeSliceCodecOptions(opts...) + + codec := SliceCodec{} + if sliceOpt.EncodeNilAsEmpty != nil { + codec.EncodeNilAsEmpty = *sliceOpt.EncodeNilAsEmpty + } + return &codec +} + +// EncodeValue is the ValueEncoder for slice types. +func (sc SliceCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Slice { + return ValueEncoderError{Name: "SliceEncodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: val} + } + + if val.IsNil() && !sc.EncodeNilAsEmpty { + return vw.WriteNull() + } + + // If we have a []byte we want to treat it as a binary instead of as an array. + if val.Type().Elem() == tByte { + var byteSlice []byte + for idx := 0; idx < val.Len(); idx++ { + byteSlice = append(byteSlice, val.Index(idx).Interface().(byte)) + } + return vw.WriteBinary(byteSlice) + } + + // If we have a []primitive.E we want to treat it as a document instead of as an array. + if val.Type().ConvertibleTo(tD) { + d := val.Convert(tD).Interface().(primitive.D) + + dw, err := vw.WriteDocument() + if err != nil { + return err + } + + for _, e := range d { + err = encodeElement(ec, dw, e) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() + } + + aw, err := vw.WriteArray() + if err != nil { + return err + } + + elemType := val.Type().Elem() + encoder, err := ec.LookupEncoder(elemType) + if err != nil && elemType.Kind() != reflect.Interface { + return err + } + + for idx := 0; idx < val.Len(); idx++ { + currEncoder, currVal, lookupErr := defaultValueEncoders.lookupElementEncoder(ec, encoder, val.Index(idx)) + if lookupErr != nil && lookupErr != errInvalidValue { + return lookupErr + } + + vw, err := aw.WriteArrayElement() + if err != nil { + return err + } + + if lookupErr == errInvalidValue { + err = vw.WriteNull() + if err != nil { + return err + } + continue + } + + err = currEncoder.EncodeValue(ec, vw, currVal) + if err != nil { + return err + } + } + return aw.WriteArrayEnd() +} + +// DecodeValue is the ValueDecoder for slice types. +func (sc *SliceCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.Slice { + return ValueDecoderError{Name: "SliceDecodeValue", Kinds: []reflect.Kind{reflect.Slice}, Received: val} + } + + switch vrType := vr.Type(); vrType { + case bsontype.Array: + case bsontype.Null: + val.Set(reflect.Zero(val.Type())) + return vr.ReadNull() + case bsontype.Undefined: + val.Set(reflect.Zero(val.Type())) + return vr.ReadUndefined() + case bsontype.Type(0), bsontype.EmbeddedDocument: + if val.Type().Elem() != tE { + return fmt.Errorf("cannot decode document into %s", val.Type()) + } + case bsontype.Binary: + if val.Type().Elem() != tByte { + return fmt.Errorf("SliceDecodeValue can only decode a binary into a byte array, got %v", vrType) + } + data, subtype, err := vr.ReadBinary() + if err != nil { + return err + } + if subtype != bsontype.BinaryGeneric && subtype != bsontype.BinaryBinaryOld { + return fmt.Errorf("SliceDecodeValue can only be used to decode subtype 0x00 or 0x02 for %s, got %v", bsontype.Binary, subtype) + } + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, len(data))) + } + + val.SetLen(0) + for _, elem := range data { + val.Set(reflect.Append(val, reflect.ValueOf(elem))) + } + return nil + case bsontype.String: + if sliceType := val.Type().Elem(); sliceType != tByte { + return fmt.Errorf("SliceDecodeValue can only decode a string into a byte array, got %v", sliceType) + } + str, err := vr.ReadString() + if err != nil { + return err + } + byteStr := []byte(str) + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, len(byteStr))) + } + + val.SetLen(0) + for _, elem := range byteStr { + val.Set(reflect.Append(val, reflect.ValueOf(elem))) + } + return nil + default: + return fmt.Errorf("cannot decode %v into a slice", vrType) + } + + var elemsFunc func(DecodeContext, bsonrw.ValueReader, reflect.Value) ([]reflect.Value, error) + switch val.Type().Elem() { + case tE: + dc.Ancestor = val.Type() + elemsFunc = defaultValueDecoders.decodeD + default: + elemsFunc = defaultValueDecoders.decodeDefault + } + + elems, err := elemsFunc(dc, vr, val) + if err != nil { + return err + } + + if val.IsNil() { + val.Set(reflect.MakeSlice(val.Type(), 0, len(elems))) + } + + val.SetLen(0) + val.Set(reflect.Append(val, elems...)) + + return nil +} diff --git a/mongo/bson/bsoncodec/string_codec.go b/mongo/bson/bsoncodec/string_codec.go new file mode 100644 index 0000000..5332b7c --- /dev/null +++ b/mongo/bson/bsoncodec/string_codec.go @@ -0,0 +1,119 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// StringCodec is the Codec used for struct values. +type StringCodec struct { + DecodeObjectIDAsHex bool +} + +var ( + defaultStringCodec = NewStringCodec() + + _ ValueCodec = defaultStringCodec + _ typeDecoder = defaultStringCodec +) + +// NewStringCodec returns a StringCodec with options opts. +func NewStringCodec(opts ...*bsonoptions.StringCodecOptions) *StringCodec { + stringOpt := bsonoptions.MergeStringCodecOptions(opts...) + return &StringCodec{*stringOpt.DecodeObjectIDAsHex} +} + +// EncodeValue is the ValueEncoder for string types. +func (sc *StringCodec) EncodeValue(ectx EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if val.Kind() != reflect.String { + return ValueEncoderError{ + Name: "StringEncodeValue", + Kinds: []reflect.Kind{reflect.String}, + Received: val, + } + } + + return vw.WriteString(val.String()) +} + +func (sc *StringCodec) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t.Kind() != reflect.String { + return emptyValue, ValueDecoderError{ + Name: "StringDecodeValue", + Kinds: []reflect.Kind{reflect.String}, + Received: reflect.Zero(t), + } + } + + var str string + var err error + switch vr.Type() { + case bsontype.String: + str, err = vr.ReadString() + if err != nil { + return emptyValue, err + } + case bsontype.ObjectID: + oid, err := vr.ReadObjectID() + if err != nil { + return emptyValue, err + } + if sc.DecodeObjectIDAsHex { + str = oid.Hex() + } else { + byteArray := [12]byte(oid) + str = string(byteArray[:]) + } + case bsontype.Symbol: + str, err = vr.ReadSymbol() + if err != nil { + return emptyValue, err + } + case bsontype.Binary: + data, subtype, err := vr.ReadBinary() + if err != nil { + return emptyValue, err + } + if subtype != bsontype.BinaryGeneric && subtype != bsontype.BinaryBinaryOld { + return emptyValue, decodeBinaryError{subtype: subtype, typeName: "string"} + } + str = string(data) + case bsontype.Null: + if err = vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err = vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into a string type", vr.Type()) + } + + return reflect.ValueOf(str), nil +} + +// DecodeValue is the ValueDecoder for string types. +func (sc *StringCodec) DecodeValue(dctx DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.String { + return ValueDecoderError{Name: "StringDecodeValue", Kinds: []reflect.Kind{reflect.String}, Received: val} + } + + elem, err := sc.decodeType(dctx, vr, val.Type()) + if err != nil { + return err + } + + val.SetString(elem.String()) + return nil +} diff --git a/mongo/bson/bsoncodec/string_codec_test.go b/mongo/bson/bsoncodec/string_codec_test.go new file mode 100644 index 0000000..59ff56f --- /dev/null +++ b/mongo/bson/bsoncodec/string_codec_test.go @@ -0,0 +1,48 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "testing" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/internal/testutil/assert" +) + +func TestStringCodec(t *testing.T) { + t.Run("ObjectIDAsHex", func(t *testing.T) { + oid := primitive.NewObjectID() + byteArray := [12]byte(oid) + reader := &bsonrwtest.ValueReaderWriter{BSONType: bsontype.ObjectID, Return: oid} + testCases := []struct { + name string + opts *bsonoptions.StringCodecOptions + hex bool + result string + }{ + {"default", bsonoptions.StringCodec(), true, oid.Hex()}, + {"true", bsonoptions.StringCodec().SetDecodeObjectIDAsHex(true), true, oid.Hex()}, + {"false", bsonoptions.StringCodec().SetDecodeObjectIDAsHex(false), false, string(byteArray[:])}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + stringCodec := NewStringCodec(tc.opts) + + actual := reflect.New(reflect.TypeOf("")).Elem() + err := stringCodec.DecodeValue(DecodeContext{}, reader, actual) + assert.Nil(t, err, "StringCodec.DecodeValue error: %v", err) + + actualString := actual.Interface().(string) + assert.Equal(t, tc.result, actualString, "Expected string %v, got %v", tc.result, actualString) + }) + } + }) +} diff --git a/mongo/bson/bsoncodec/struct_codec.go b/mongo/bson/bsoncodec/struct_codec.go new file mode 100644 index 0000000..da1ae18 --- /dev/null +++ b/mongo/bson/bsoncodec/struct_codec.go @@ -0,0 +1,669 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// DecodeError represents an error that occurs when unmarshalling BSON bytes into a native Go type. +type DecodeError struct { + keys []string + wrapped error +} + +// Unwrap returns the underlying error +func (de *DecodeError) Unwrap() error { + return de.wrapped +} + +// Error implements the error interface. +func (de *DecodeError) Error() string { + // The keys are stored in reverse order because the de.keys slice is builtup while propagating the error up the + // stack of BSON keys, so we call de.Keys(), which reverses them. + keyPath := strings.Join(de.Keys(), ".") + return fmt.Sprintf("error decoding key %s: %v", keyPath, de.wrapped) +} + +// Keys returns the BSON key path that caused an error as a slice of strings. The keys in the slice are in top-down +// order. For example, if the document being unmarshalled was {a: {b: {c: 1}}} and the value for c was supposed to be +// a string, the keys slice will be ["a", "b", "c"]. +func (de *DecodeError) Keys() []string { + reversedKeys := make([]string, 0, len(de.keys)) + for idx := len(de.keys) - 1; idx >= 0; idx-- { + reversedKeys = append(reversedKeys, de.keys[idx]) + } + + return reversedKeys +} + +// Zeroer allows custom struct types to implement a report of zero +// state. All struct types that don't implement Zeroer or where IsZero +// returns false are considered to be not zero. +type Zeroer interface { + IsZero() bool +} + +// StructCodec is the Codec used for struct values. +type StructCodec struct { + cache map[reflect.Type]*structDescription + l sync.RWMutex + parser StructTagParser + DecodeZeroStruct bool + DecodeDeepZeroInline bool + EncodeOmitDefaultStruct bool + AllowUnexportedFields bool + OverwriteDuplicatedInlinedFields bool +} + +var _ ValueEncoder = &StructCodec{} +var _ ValueDecoder = &StructCodec{} + +// NewStructCodec returns a StructCodec that uses p for struct tag parsing. +func NewStructCodec(p StructTagParser, opts ...*bsonoptions.StructCodecOptions) (*StructCodec, error) { + if p == nil { + return nil, errors.New("a StructTagParser must be provided to NewStructCodec") + } + + structOpt := bsonoptions.MergeStructCodecOptions(opts...) + + codec := &StructCodec{ + cache: make(map[reflect.Type]*structDescription), + parser: p, + } + + if structOpt.DecodeZeroStruct != nil { + codec.DecodeZeroStruct = *structOpt.DecodeZeroStruct + } + if structOpt.DecodeDeepZeroInline != nil { + codec.DecodeDeepZeroInline = *structOpt.DecodeDeepZeroInline + } + if structOpt.EncodeOmitDefaultStruct != nil { + codec.EncodeOmitDefaultStruct = *structOpt.EncodeOmitDefaultStruct + } + if structOpt.OverwriteDuplicatedInlinedFields != nil { + codec.OverwriteDuplicatedInlinedFields = *structOpt.OverwriteDuplicatedInlinedFields + } + if structOpt.AllowUnexportedFields != nil { + codec.AllowUnexportedFields = *structOpt.AllowUnexportedFields + } + + return codec, nil +} + +// EncodeValue handles encoding generic struct types. +func (sc *StructCodec) EncodeValue(r EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Kind() != reflect.Struct { + return ValueEncoderError{Name: "StructCodec.EncodeValue", Kinds: []reflect.Kind{reflect.Struct}, Received: val} + } + + sd, err := sc.describeStruct(r.Registry, val.Type()) + if err != nil { + return err + } + + dw, err := vw.WriteDocument() + if err != nil { + return err + } + var rv reflect.Value + for _, desc := range sd.fl { + if desc.inline == nil { + rv = val.Field(desc.idx) + } else { + rv, err = fieldByIndexErr(val, desc.inline) + if err != nil { + continue + } + } + + desc.encoder, rv, err = defaultValueEncoders.lookupElementEncoder(r, desc.encoder, rv) + + if err != nil && err != errInvalidValue { + return err + } + + if err == errInvalidValue { + if desc.omitEmpty { + continue + } + vw2, err := dw.WriteDocumentElement(desc.name) + if err != nil { + return err + } + err = vw2.WriteNull() + if err != nil { + return err + } + continue + } + + if desc.encoder == nil { + return ErrNoEncoder{Type: rv.Type()} + } + + encoder := desc.encoder + + var isZero bool + rvInterface := rv.Interface() + if cz, ok := encoder.(CodecZeroer); ok { + isZero = cz.IsTypeZero(rvInterface) + } else if rv.Kind() == reflect.Interface { + // sc.isZero will not treat an interface rv as an interface, so we need to check for the zero interface separately. + isZero = rv.IsNil() + } else { + isZero = sc.isZero(rvInterface) + } + if desc.omitEmpty && isZero { + continue + } + + vw2, err := dw.WriteDocumentElement(desc.name) + if err != nil { + return err + } + + ectx := EncodeContext{Registry: r.Registry, MinSize: desc.minSize} + err = encoder.EncodeValue(ectx, vw2, rv) + if err != nil { + return err + } + } + + if sd.inlineMap >= 0 { + rv := val.Field(sd.inlineMap) + collisionFn := func(key string) bool { + _, exists := sd.fm[key] + return exists + } + + return defaultMapCodec.mapEncodeValue(r, dw, rv, collisionFn) + } + + return dw.WriteDocumentEnd() +} + +func newDecodeError(key string, original error) error { + de, ok := original.(*DecodeError) + if !ok { + return &DecodeError{ + keys: []string{key}, + wrapped: original, + } + } + + de.keys = append(de.keys, key) + return de +} + +// DecodeValue implements the Codec interface. +// By default, map types in val will not be cleared. If a map has existing key/value pairs, it will be extended with the new ones from vr. +// For slices, the decoder will set the length of the slice to zero and append all elements. The underlying array will not be cleared. +func (sc *StructCodec) DecodeValue(r DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Kind() != reflect.Struct { + return ValueDecoderError{Name: "StructCodec.DecodeValue", Kinds: []reflect.Kind{reflect.Struct}, Received: val} + } + + switch vrType := vr.Type(); vrType { + case bsontype.Type(0), bsontype.EmbeddedDocument: + case bsontype.Null: + if err := vr.ReadNull(); err != nil { + return err + } + + val.Set(reflect.Zero(val.Type())) + return nil + case bsontype.Undefined: + if err := vr.ReadUndefined(); err != nil { + return err + } + + val.Set(reflect.Zero(val.Type())) + return nil + default: + return fmt.Errorf("cannot decode %v into a %s", vrType, val.Type()) + } + + sd, err := sc.describeStruct(r.Registry, val.Type()) + if err != nil { + return err + } + + if sc.DecodeZeroStruct { + val.Set(reflect.Zero(val.Type())) + } + if sc.DecodeDeepZeroInline && sd.inline { + val.Set(deepZero(val.Type())) + } + + var decoder ValueDecoder + var inlineMap reflect.Value + if sd.inlineMap >= 0 { + inlineMap = val.Field(sd.inlineMap) + decoder, err = r.LookupDecoder(inlineMap.Type().Elem()) + if err != nil { + return err + } + } + + dr, err := vr.ReadDocument() + if err != nil { + return err + } + + for { + name, vr, err := dr.ReadElement() + if err == bsonrw.ErrEOD { + break + } + if err != nil { + return err + } + + fd, exists := sd.fm[name] + if !exists { + // if the original name isn't found in the struct description, try again with the name in lowercase + // this could match if a BSON tag isn't specified because by default, describeStruct lowercases all field + // names + fd, exists = sd.fm[strings.ToLower(name)] + } + + if !exists { + if sd.inlineMap < 0 { + // The encoding/json package requires a flag to return on error for non-existent fields. + // This functionality seems appropriate for the struct codec. + err = vr.Skip() + if err != nil { + return err + } + continue + } + + if inlineMap.IsNil() { + inlineMap.Set(reflect.MakeMap(inlineMap.Type())) + } + + elem := reflect.New(inlineMap.Type().Elem()).Elem() + r.Ancestor = inlineMap.Type() + err = decoder.DecodeValue(r, vr, elem) + if err != nil { + return err + } + inlineMap.SetMapIndex(reflect.ValueOf(name), elem) + continue + } + + var field reflect.Value + if fd.inline == nil { + field = val.Field(fd.idx) + } else { + field, err = getInlineField(val, fd.inline) + if err != nil { + return err + } + } + + if !field.CanSet() { // Being settable is a super set of being addressable. + innerErr := fmt.Errorf("field %v is not settable", field) + return newDecodeError(fd.name, innerErr) + } + if field.Kind() == reflect.Ptr && field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + field = field.Addr() + + dctx := DecodeContext{ + Registry: r.Registry, + Truncate: fd.truncate || r.Truncate, + defaultDocumentType: r.defaultDocumentType, + } + + if fd.decoder == nil { + return newDecodeError(fd.name, ErrNoDecoder{Type: field.Elem().Type()}) + } + + err = fd.decoder.DecodeValue(dctx, vr, field.Elem()) + if err != nil { + return newDecodeError(fd.name, err) + } + } + + return nil +} + +func (sc *StructCodec) isZero(i interface{}) bool { + v := reflect.ValueOf(i) + + // check the value validity + if !v.IsValid() { + return true + } + + if z, ok := v.Interface().(Zeroer); ok && (v.Kind() != reflect.Ptr || !v.IsNil()) { + return z.IsZero() + } + + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Struct: + if sc.EncodeOmitDefaultStruct { + vt := v.Type() + if vt == tTime { + return v.Interface().(time.Time).IsZero() + } + for i := 0; i < v.NumField(); i++ { + if vt.Field(i).PkgPath != "" && !vt.Field(i).Anonymous { + continue // Private field + } + fld := v.Field(i) + if !sc.isZero(fld.Interface()) { + return false + } + } + return true + } + } + + return false +} + +type structDescription struct { + fm map[string]fieldDescription + fl []fieldDescription + inlineMap int + inline bool +} + +type fieldDescription struct { + name string // BSON key name + fieldName string // struct field name + idx int + omitEmpty bool + minSize bool + truncate bool + inline []int + encoder ValueEncoder + decoder ValueDecoder +} + +type byIndex []fieldDescription + +func (bi byIndex) Len() int { return len(bi) } + +func (bi byIndex) Swap(i, j int) { bi[i], bi[j] = bi[j], bi[i] } + +func (bi byIndex) Less(i, j int) bool { + // If a field is inlined, its index in the top level struct is stored at inline[0] + iIdx, jIdx := bi[i].idx, bi[j].idx + if len(bi[i].inline) > 0 { + iIdx = bi[i].inline[0] + } + if len(bi[j].inline) > 0 { + jIdx = bi[j].inline[0] + } + if iIdx != jIdx { + return iIdx < jIdx + } + for k, biik := range bi[i].inline { + if k >= len(bi[j].inline) { + return false + } + if biik != bi[j].inline[k] { + return biik < bi[j].inline[k] + } + } + return len(bi[i].inline) < len(bi[j].inline) +} + +func (sc *StructCodec) describeStruct(r *Registry, t reflect.Type) (*structDescription, error) { + // We need to analyze the struct, including getting the tags, collecting + // information about inlining, and create a map of the field name to the field. + sc.l.RLock() + ds, exists := sc.cache[t] + sc.l.RUnlock() + if exists { + return ds, nil + } + + numFields := t.NumField() + sd := &structDescription{ + fm: make(map[string]fieldDescription, numFields), + fl: make([]fieldDescription, 0, numFields), + inlineMap: -1, + } + + var fields []fieldDescription + for i := 0; i < numFields; i++ { + sf := t.Field(i) + if sf.PkgPath != "" && (!sc.AllowUnexportedFields || !sf.Anonymous) { + // field is private or unexported fields aren't allowed, ignore + continue + } + + sfType := sf.Type + encoder, err := r.LookupEncoder(sfType) + if err != nil { + encoder = nil + } + decoder, err := r.LookupDecoder(sfType) + if err != nil { + decoder = nil + } + + description := fieldDescription{ + fieldName: sf.Name, + idx: i, + encoder: encoder, + decoder: decoder, + } + + stags, err := sc.parser.ParseStructTags(sf) + if err != nil { + return nil, err + } + if stags.Skip { + continue + } + description.name = stags.Name + description.omitEmpty = stags.OmitEmpty + description.minSize = stags.MinSize + description.truncate = stags.Truncate + + if stags.Inline { + sd.inline = true + switch sfType.Kind() { + case reflect.Map: + if sd.inlineMap >= 0 { + return nil, errors.New("(struct " + t.String() + ") multiple inline maps") + } + if sfType.Key() != tString { + return nil, errors.New("(struct " + t.String() + ") inline map must have a string keys") + } + sd.inlineMap = description.idx + case reflect.Ptr: + sfType = sfType.Elem() + if sfType.Kind() != reflect.Struct { + return nil, fmt.Errorf("(struct %s) inline fields must be a struct, a struct pointer, or a map", t.String()) + } + fallthrough + case reflect.Struct: + inlinesf, err := sc.describeStruct(r, sfType) + if err != nil { + return nil, err + } + for _, fd := range inlinesf.fl { + if fd.inline == nil { + fd.inline = []int{i, fd.idx} + } else { + fd.inline = append([]int{i}, fd.inline...) + } + fields = append(fields, fd) + + } + default: + return nil, fmt.Errorf("(struct %s) inline fields must be a struct, a struct pointer, or a map", t.String()) + } + continue + } + fields = append(fields, description) + } + + // Sort fieldDescriptions by name and use dominance rules to determine which should be added for each name + sort.Slice(fields, func(i, j int) bool { + x := fields + // sort field by name, breaking ties with depth, then + // breaking ties with index sequence. + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].inline) != len(x[j].inline) { + return len(x[i].inline) < len(x[j].inline) + } + return byIndex(x).Less(i, j) + }) + + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + sd.fl = append(sd.fl, fi) + sd.fm[name] = fi + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if !ok || !sc.OverwriteDuplicatedInlinedFields { + return nil, fmt.Errorf("struct %s has duplicated key %s", t.String(), name) + } + sd.fl = append(sd.fl, dominant) + sd.fm[name] = dominant + } + + sort.Sort(byIndex(sd.fl)) + + sc.l.Lock() + sc.cache[t] = sd + sc.l.Unlock() + + return sd, nil +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's inlining rules. If there are multiple top-level +// fields, the boolean will be false: This condition is an error in Go +// and we skip all the fields. +func dominantField(fields []fieldDescription) (fieldDescription, bool) { + // The fields are sorted in increasing index-length order, then by presence of tag. + // That means that the first field is the dominant one. We need only check + // for error cases: two fields at top level. + if len(fields) > 1 && + len(fields[0].inline) == len(fields[1].inline) { + return fieldDescription{}, false + } + return fields[0], true +} + +func fieldByIndexErr(v reflect.Value, index []int) (result reflect.Value, err error) { + defer func() { + if recovered := recover(); recovered != nil { + switch r := recovered.(type) { + case string: + err = fmt.Errorf("%s", r) + case error: + err = r + } + } + }() + + result = v.FieldByIndex(index) + return +} + +func getInlineField(val reflect.Value, index []int) (reflect.Value, error) { + field, err := fieldByIndexErr(val, index) + if err == nil { + return field, nil + } + + // if parent of this element doesn't exist, fix its parent + inlineParent := index[:len(index)-1] + var fParent reflect.Value + if fParent, err = fieldByIndexErr(val, inlineParent); err != nil { + fParent, err = getInlineField(val, inlineParent) + if err != nil { + return fParent, err + } + } + fParent.Set(reflect.New(fParent.Type().Elem())) + + return fieldByIndexErr(val, index) +} + +// DeepZero returns recursive zero object +func deepZero(st reflect.Type) (result reflect.Value) { + result = reflect.Indirect(reflect.New(st)) + + if result.Kind() == reflect.Struct { + for i := 0; i < result.NumField(); i++ { + if f := result.Field(i); f.Kind() == reflect.Ptr { + if f.CanInterface() { + if ft := reflect.TypeOf(f.Interface()); ft.Elem().Kind() == reflect.Struct { + result.Field(i).Set(recursivePointerTo(deepZero(ft.Elem()))) + } + } + } + } + } + + return +} + +// recursivePointerTo calls reflect.New(v.Type) but recursively for its fields inside +func recursivePointerTo(v reflect.Value) reflect.Value { + v = reflect.Indirect(v) + result := reflect.New(v.Type()) + if v.Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + if f := v.Field(i); f.Kind() == reflect.Ptr { + if f.Elem().Kind() == reflect.Struct { + result.Elem().Field(i).Set(recursivePointerTo(f)) + } + } + } + } + + return result +} diff --git a/mongo/bson/bsoncodec/struct_codec_test.go b/mongo/bson/bsoncodec/struct_codec_test.go new file mode 100644 index 0000000..cf49541 --- /dev/null +++ b/mongo/bson/bsoncodec/struct_codec_test.go @@ -0,0 +1,47 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestZeoerInterfaceUsedByDecoder(t *testing.T) { + enc := &StructCodec{} + + // cases that are zero, because they are known types or pointers + var st *nonZeroer + assert.True(t, enc.isZero(st)) + assert.True(t, enc.isZero(0)) + assert.True(t, enc.isZero(false)) + + // cases that shouldn't be zero + st = &nonZeroer{value: false} + assert.False(t, enc.isZero(struct{ val bool }{val: true})) + assert.False(t, enc.isZero(struct{ val bool }{val: false})) + assert.False(t, enc.isZero(st)) + st.value = true + assert.False(t, enc.isZero(st)) + + // a test to see if the interface impacts the outcome + z := zeroTest{} + assert.False(t, enc.isZero(z)) + + z.reportZero = true + assert.True(t, enc.isZero(z)) + + // *time.Time with nil should be zero + var tp *time.Time + assert.True(t, enc.isZero(tp)) + + // actually all zeroer if nil should also be zero + var zp *zeroTest + assert.True(t, enc.isZero(zp)) +} diff --git a/mongo/bson/bsoncodec/struct_tag_parser.go b/mongo/bson/bsoncodec/struct_tag_parser.go new file mode 100644 index 0000000..62708c5 --- /dev/null +++ b/mongo/bson/bsoncodec/struct_tag_parser.go @@ -0,0 +1,139 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "strings" +) + +// StructTagParser returns the struct tags for a given struct field. +type StructTagParser interface { + ParseStructTags(reflect.StructField) (StructTags, error) +} + +// StructTagParserFunc is an adapter that allows a generic function to be used +// as a StructTagParser. +type StructTagParserFunc func(reflect.StructField) (StructTags, error) + +// ParseStructTags implements the StructTagParser interface. +func (stpf StructTagParserFunc) ParseStructTags(sf reflect.StructField) (StructTags, error) { + return stpf(sf) +} + +// StructTags represents the struct tag fields that the StructCodec uses during +// the encoding and decoding process. +// +// In the case of a struct, the lowercased field name is used as the key for each exported +// field but this behavior may be changed using a struct tag. The tag may also contain flags to +// adjust the marshalling behavior for the field. +// +// The properties are defined below: +// +// OmitEmpty Only include the field if it's not set to the zero value for the type or to +// empty slices or maps. +// +// MinSize Marshal an integer of a type larger than 32 bits value as an int32, if that's +// feasible while preserving the numeric value. +// +// Truncate When unmarshaling a BSON double, it is permitted to lose precision to fit within +// a float32. +// +// Inline Inline the field, which must be a struct or a map, causing all of its fields +// or keys to be processed as if they were part of the outer struct. For maps, +// keys must not conflict with the bson keys of other struct fields. +// +// Skip This struct field should be skipped. This is usually denoted by parsing a "-" +// for the name. +// +// TODO(skriptble): Add tags for undefined as nil and for null as nil. +type StructTags struct { + Name string + OmitEmpty bool + MinSize bool + Truncate bool + Inline bool + Skip bool +} + +// DefaultStructTagParser is the StructTagParser used by the StructCodec by default. +// It will handle the bson struct tag. See the documentation for StructTags to see +// what each of the returned fields means. +// +// If there is no name in the struct tag fields, the struct field name is lowercased. +// The tag formats accepted are: +// +// "[][,[,]]" +// +// `(...) bson:"[][,[,]]" (...)` +// +// An example: +// +// type T struct { +// A bool +// B int "myb" +// C string "myc,omitempty" +// D string `bson:",omitempty" json:"jsonkey"` +// E int64 ",minsize" +// F int64 "myf,omitempty,minsize" +// } +// +// A struct tag either consisting entirely of '-' or with a bson key with a +// value consisting entirely of '-' will return a StructTags with Skip true and +// the remaining fields will be their default values. +var DefaultStructTagParser StructTagParserFunc = func(sf reflect.StructField) (StructTags, error) { + key := strings.ToLower(sf.Name) + tag, ok := sf.Tag.Lookup("bson") + if !ok && !strings.Contains(string(sf.Tag), ":") && len(sf.Tag) > 0 { + tag = string(sf.Tag) + } + return parseTags(key, tag) +} + +func parseTags(key string, tag string) (StructTags, error) { + var st StructTags + if tag == "-" { + st.Skip = true + return st, nil + } + + for idx, str := range strings.Split(tag, ",") { + if idx == 0 && str != "" { + key = str + } + switch str { + case "omitempty": + st.OmitEmpty = true + case "minsize": + st.MinSize = true + case "truncate": + st.Truncate = true + case "inline": + st.Inline = true + } + } + + st.Name = key + + return st, nil +} + +// JSONFallbackStructTagParser has the same behavior as DefaultStructTagParser +// but will also fallback to parsing the json tag instead on a field where the +// bson tag isn't available. +var JSONFallbackStructTagParser StructTagParserFunc = func(sf reflect.StructField) (StructTags, error) { + key := strings.ToLower(sf.Name) + tag, ok := sf.Tag.Lookup("bson") + if !ok { + tag, ok = sf.Tag.Lookup("json") + } + if !ok && !strings.Contains(string(sf.Tag), ":") && len(sf.Tag) > 0 { + tag = string(sf.Tag) + } + + return parseTags(key, tag) +} diff --git a/mongo/bson/bsoncodec/struct_tag_parser_test.go b/mongo/bson/bsoncodec/struct_tag_parser_test.go new file mode 100644 index 0000000..c60d8f9 --- /dev/null +++ b/mongo/bson/bsoncodec/struct_tag_parser_test.go @@ -0,0 +1,160 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStructTagParsers(t *testing.T) { + testCases := []struct { + name string + sf reflect.StructField + want StructTags + parser StructTagParserFunc + }{ + { + "default no bson tag", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("bar")}, + StructTags{Name: "bar"}, + DefaultStructTagParser, + }, + { + "default empty", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("")}, + StructTags{Name: "foo"}, + DefaultStructTagParser, + }, + { + "default tag only dash", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("-")}, + StructTags{Skip: true}, + DefaultStructTagParser, + }, + { + "default bson tag only dash", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"-"`)}, + StructTags{Skip: true}, + DefaultStructTagParser, + }, + { + "default all options", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, + StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + DefaultStructTagParser, + }, + { + "default all options default name", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, + StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + DefaultStructTagParser, + }, + { + "default bson tag all options", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + DefaultStructTagParser, + }, + { + "default bson tag all options default name", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + DefaultStructTagParser, + }, + { + "default ignore xml", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`xml:"bar"`)}, + StructTags{Name: "foo"}, + DefaultStructTagParser, + }, + { + "JSONFallback no bson tag", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("bar")}, + StructTags{Name: "bar"}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback empty", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("")}, + StructTags{Name: "foo"}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback tag only dash", + reflect.StructField{Name: "foo", Tag: reflect.StructTag("-")}, + StructTags{Skip: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback bson tag only dash", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"-"`)}, + StructTags{Skip: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback all options", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, + StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback all options default name", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, + StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback bson tag all options", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback bson tag all options default name", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback json tag all options", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback json tag all options default name", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,minsize,truncate,inline"`)}, + StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback bson tag overrides other tags", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar" json:"qux,truncate"`)}, + StructTags{Name: "bar"}, + JSONFallbackStructTagParser, + }, + { + "JSONFallback ignore xml", + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`xml:"bar"`)}, + StructTags{Name: "foo"}, + JSONFallbackStructTagParser, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parser(tc.sf) + noerr(t, err) + if !cmp.Equal(got, tc.want) { + t.Errorf("Returned struct tags do not match. got %#v; want %#v", got, tc.want) + } + }) + } +} diff --git a/mongo/bson/bsoncodec/time_codec.go b/mongo/bson/bsoncodec/time_codec.go new file mode 100644 index 0000000..ec7e30f --- /dev/null +++ b/mongo/bson/bsoncodec/time_codec.go @@ -0,0 +1,127 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "reflect" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + timeFormatString = "2006-01-02T15:04:05.999Z07:00" +) + +// TimeCodec is the Codec used for time.Time values. +type TimeCodec struct { + UseLocalTimeZone bool +} + +var ( + defaultTimeCodec = NewTimeCodec() + + _ ValueCodec = defaultTimeCodec + _ typeDecoder = defaultTimeCodec +) + +// NewTimeCodec returns a TimeCodec with options opts. +func NewTimeCodec(opts ...*bsonoptions.TimeCodecOptions) *TimeCodec { + timeOpt := bsonoptions.MergeTimeCodecOptions(opts...) + + codec := TimeCodec{} + if timeOpt.UseLocalTimeZone != nil { + codec.UseLocalTimeZone = *timeOpt.UseLocalTimeZone + } + return &codec +} + +func (tc *TimeCodec) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + if t != tTime { + return emptyValue, ValueDecoderError{ + Name: "TimeDecodeValue", + Types: []reflect.Type{tTime}, + Received: reflect.Zero(t), + } + } + + var timeVal time.Time + switch vrType := vr.Type(); vrType { + case bsontype.DateTime: + dt, err := vr.ReadDateTime() + if err != nil { + return emptyValue, err + } + timeVal = time.Unix(dt/1000, dt%1000*1000000) + case bsontype.String: + // assume strings are in the isoTimeFormat + timeStr, err := vr.ReadString() + if err != nil { + return emptyValue, err + } + timeVal, err = time.Parse(timeFormatString, timeStr) + if err != nil { + return emptyValue, err + } + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return emptyValue, err + } + timeVal = time.Unix(i64/1000, i64%1000*1000000) + case bsontype.Timestamp: + t, _, err := vr.ReadTimestamp() + if err != nil { + return emptyValue, err + } + timeVal = time.Unix(int64(t), 0) + case bsontype.Null: + if err := vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err := vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into a time.Time", vrType) + } + + if !tc.UseLocalTimeZone { + timeVal = timeVal.UTC() + } + return reflect.ValueOf(timeVal), nil +} + +// DecodeValue is the ValueDecoderFunc for time.Time. +func (tc *TimeCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() || val.Type() != tTime { + return ValueDecoderError{Name: "TimeDecodeValue", Types: []reflect.Type{tTime}, Received: val} + } + + elem, err := tc.decodeType(dc, vr, tTime) + if err != nil { + return err + } + + val.Set(elem) + return nil +} + +// EncodeValue is the ValueEncoderFunc for time.TIme. +func (tc *TimeCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != tTime { + return ValueEncoderError{Name: "TimeEncodeValue", Types: []reflect.Type{tTime}, Received: val} + } + tt := val.Interface().(time.Time) + dt := primitive.NewDateTimeFromTime(tt) + return vw.WriteDateTime(int64(dt)) +} diff --git a/mongo/bson/bsoncodec/time_codec_test.go b/mongo/bson/bsoncodec/time_codec_test.go new file mode 100644 index 0000000..189a748 --- /dev/null +++ b/mongo/bson/bsoncodec/time_codec_test.go @@ -0,0 +1,79 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "reflect" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/internal/testutil/assert" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +func TestTimeCodec(t *testing.T) { + now := time.Now().Truncate(time.Millisecond) + + t.Run("UseLocalTimeZone", func(t *testing.T) { + reader := &bsonrwtest.ValueReaderWriter{BSONType: bsontype.DateTime, Return: now.UnixNano() / int64(time.Millisecond)} + testCases := []struct { + name string + opts *bsonoptions.TimeCodecOptions + utc bool + }{ + {"default", bsonoptions.TimeCodec(), true}, + {"false", bsonoptions.TimeCodec().SetUseLocalTimeZone(false), true}, + {"true", bsonoptions.TimeCodec().SetUseLocalTimeZone(true), false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + timeCodec := NewTimeCodec(tc.opts) + + actual := reflect.New(reflect.TypeOf(now)).Elem() + err := timeCodec.DecodeValue(DecodeContext{}, reader, actual) + assert.Nil(t, err, "TimeCodec.DecodeValue error: %v", err) + + actualTime := actual.Interface().(time.Time) + assert.Equal(t, actualTime.Location().String() == "UTC", tc.utc, + "Expected UTC: %v, got %v", tc.utc, actualTime.Location()) + assert.Equal(t, now, actualTime, "expected time %v, got %v", now, actualTime) + }) + } + }) + + t.Run("DecodeFromBsontype", func(t *testing.T) { + testCases := []struct { + name string + reader *bsonrwtest.ValueReaderWriter + }{ + {"string", &bsonrwtest.ValueReaderWriter{BSONType: bsontype.String, Return: now.Format(timeFormatString)}}, + {"int64", &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Int64, Return: now.Unix()*1000 + int64(now.Nanosecond()/1e6)}}, + {"timestamp", &bsonrwtest.ValueReaderWriter{BSONType: bsontype.Timestamp, + Return: bsoncore.Value{ + Type: bsontype.Timestamp, + Data: bsoncore.AppendTimestamp(nil, uint32(now.Unix()), 0), + }}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := reflect.New(reflect.TypeOf(now)).Elem() + err := defaultTimeCodec.DecodeValue(DecodeContext{}, tc.reader, actual) + assert.Nil(t, err, "DecodeValue error: %v", err) + + actualTime := actual.Interface().(time.Time) + if tc.name == "timestamp" { + now = time.Unix(now.Unix(), 0) + } + assert.Equal(t, now, actualTime, "expected time %v, got %v", now, actualTime) + }) + } + }) +} diff --git a/mongo/bson/bsoncodec/types.go b/mongo/bson/bsoncodec/types.go new file mode 100644 index 0000000..07f4b70 --- /dev/null +++ b/mongo/bson/bsoncodec/types.go @@ -0,0 +1,57 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "encoding/json" + "net/url" + "reflect" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +var tBool = reflect.TypeOf(false) +var tFloat64 = reflect.TypeOf(float64(0)) +var tInt32 = reflect.TypeOf(int32(0)) +var tInt64 = reflect.TypeOf(int64(0)) +var tString = reflect.TypeOf("") +var tTime = reflect.TypeOf(time.Time{}) + +var tEmpty = reflect.TypeOf((*interface{})(nil)).Elem() +var tByteSlice = reflect.TypeOf([]byte(nil)) +var tByte = reflect.TypeOf(byte(0x00)) +var tURL = reflect.TypeOf(url.URL{}) +var tJSONNumber = reflect.TypeOf(json.Number("")) + +var tValueMarshaler = reflect.TypeOf((*ValueMarshaler)(nil)).Elem() +var tValueUnmarshaler = reflect.TypeOf((*ValueUnmarshaler)(nil)).Elem() +var tMarshaler = reflect.TypeOf((*Marshaler)(nil)).Elem() +var tUnmarshaler = reflect.TypeOf((*Unmarshaler)(nil)).Elem() +var tProxy = reflect.TypeOf((*Proxy)(nil)).Elem() + +var tBinary = reflect.TypeOf(primitive.Binary{}) +var tUndefined = reflect.TypeOf(primitive.Undefined{}) +var tOID = reflect.TypeOf(primitive.ObjectID{}) +var tDateTime = reflect.TypeOf(primitive.DateTime(0)) +var tNull = reflect.TypeOf(primitive.Null{}) +var tRegex = reflect.TypeOf(primitive.Regex{}) +var tCodeWithScope = reflect.TypeOf(primitive.CodeWithScope{}) +var tDBPointer = reflect.TypeOf(primitive.DBPointer{}) +var tJavaScript = reflect.TypeOf(primitive.JavaScript("")) +var tSymbol = reflect.TypeOf(primitive.Symbol("")) +var tTimestamp = reflect.TypeOf(primitive.Timestamp{}) +var tDecimal = reflect.TypeOf(primitive.Decimal128{}) +var tMinKey = reflect.TypeOf(primitive.MinKey{}) +var tMaxKey = reflect.TypeOf(primitive.MaxKey{}) +var tD = reflect.TypeOf(primitive.D{}) +var tA = reflect.TypeOf(primitive.A{}) +var tE = reflect.TypeOf(primitive.E{}) + +var tCoreDocument = reflect.TypeOf(bsoncore.Document{}) +var tCoreArray = reflect.TypeOf(bsoncore.Array{}) diff --git a/mongo/bson/bsoncodec/uint_codec.go b/mongo/bson/bsoncodec/uint_codec.go new file mode 100644 index 0000000..0b21ce9 --- /dev/null +++ b/mongo/bson/bsoncodec/uint_codec.go @@ -0,0 +1,173 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsoncodec + +import ( + "fmt" + "math" + "reflect" + + "go.mongodb.org/mongo-driver/bson/bsonoptions" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// UIntCodec is the Codec used for uint values. +type UIntCodec struct { + EncodeToMinSize bool +} + +var ( + defaultUIntCodec = NewUIntCodec() + + _ ValueCodec = defaultUIntCodec + _ typeDecoder = defaultUIntCodec +) + +// NewUIntCodec returns a UIntCodec with options opts. +func NewUIntCodec(opts ...*bsonoptions.UIntCodecOptions) *UIntCodec { + uintOpt := bsonoptions.MergeUIntCodecOptions(opts...) + + codec := UIntCodec{} + if uintOpt.EncodeToMinSize != nil { + codec.EncodeToMinSize = *uintOpt.EncodeToMinSize + } + return &codec +} + +// EncodeValue is the ValueEncoder for uint types. +func (uic *UIntCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + switch val.Kind() { + case reflect.Uint8, reflect.Uint16: + return vw.WriteInt32(int32(val.Uint())) + case reflect.Uint, reflect.Uint32, reflect.Uint64: + u64 := val.Uint() + + // If ec.MinSize or if encodeToMinSize is true for a non-uint64 value we should write val as an int32 + useMinSize := ec.MinSize || (uic.EncodeToMinSize && val.Kind() != reflect.Uint64) + + if u64 <= math.MaxInt32 && useMinSize { + return vw.WriteInt32(int32(u64)) + } + if u64 > math.MaxInt64 { + return fmt.Errorf("%d overflows int64", u64) + } + return vw.WriteInt64(int64(u64)) + } + + return ValueEncoderError{ + Name: "UintEncodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: val, + } +} + +func (uic *UIntCodec) decodeType(dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type) (reflect.Value, error) { + var i64 int64 + var err error + switch vrType := vr.Type(); vrType { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return emptyValue, err + } + i64 = int64(i32) + case bsontype.Int64: + i64, err = vr.ReadInt64() + if err != nil { + return emptyValue, err + } + case bsontype.Double: + f64, err := vr.ReadDouble() + if err != nil { + return emptyValue, err + } + if !dc.Truncate && math.Floor(f64) != f64 { + return emptyValue, errCannotTruncate + } + if f64 > float64(math.MaxInt64) { + return emptyValue, fmt.Errorf("%g overflows int64", f64) + } + i64 = int64(f64) + case bsontype.Boolean: + b, err := vr.ReadBoolean() + if err != nil { + return emptyValue, err + } + if b { + i64 = 1 + } + case bsontype.Null: + if err = vr.ReadNull(); err != nil { + return emptyValue, err + } + case bsontype.Undefined: + if err = vr.ReadUndefined(); err != nil { + return emptyValue, err + } + default: + return emptyValue, fmt.Errorf("cannot decode %v into an integer type", vrType) + } + + switch t.Kind() { + case reflect.Uint8: + if i64 < 0 || i64 > math.MaxUint8 { + return emptyValue, fmt.Errorf("%d overflows uint8", i64) + } + + return reflect.ValueOf(uint8(i64)), nil + case reflect.Uint16: + if i64 < 0 || i64 > math.MaxUint16 { + return emptyValue, fmt.Errorf("%d overflows uint16", i64) + } + + return reflect.ValueOf(uint16(i64)), nil + case reflect.Uint32: + if i64 < 0 || i64 > math.MaxUint32 { + return emptyValue, fmt.Errorf("%d overflows uint32", i64) + } + + return reflect.ValueOf(uint32(i64)), nil + case reflect.Uint64: + if i64 < 0 { + return emptyValue, fmt.Errorf("%d overflows uint64", i64) + } + + return reflect.ValueOf(uint64(i64)), nil + case reflect.Uint: + if i64 < 0 || int64(uint(i64)) != i64 { // Can we fit this inside of an uint + return emptyValue, fmt.Errorf("%d overflows uint", i64) + } + + return reflect.ValueOf(uint(i64)), nil + default: + return emptyValue, ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: reflect.Zero(t), + } + } +} + +// DecodeValue is the ValueDecoder for uint types. +func (uic *UIntCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.CanSet() { + return ValueDecoderError{ + Name: "UintDecodeValue", + Kinds: []reflect.Kind{reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint}, + Received: val, + } + } + + elem, err := uic.decodeType(dc, vr, val.Type()) + if err != nil { + return err + } + + val.SetUint(elem.Uint()) + return nil +} diff --git a/mongo/bson/bsonoptions/byte_slice_codec_options.go b/mongo/bson/bsonoptions/byte_slice_codec_options.go new file mode 100644 index 0000000..b1256a4 --- /dev/null +++ b/mongo/bson/bsonoptions/byte_slice_codec_options.go @@ -0,0 +1,38 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// ByteSliceCodecOptions represents all possible options for byte slice encoding and decoding. +type ByteSliceCodecOptions struct { + EncodeNilAsEmpty *bool // Specifies if a nil byte slice should encode as an empty binary instead of null. Defaults to false. +} + +// ByteSliceCodec creates a new *ByteSliceCodecOptions +func ByteSliceCodec() *ByteSliceCodecOptions { + return &ByteSliceCodecOptions{} +} + +// SetEncodeNilAsEmpty specifies if a nil byte slice should encode as an empty binary instead of null. Defaults to false. +func (bs *ByteSliceCodecOptions) SetEncodeNilAsEmpty(b bool) *ByteSliceCodecOptions { + bs.EncodeNilAsEmpty = &b + return bs +} + +// MergeByteSliceCodecOptions combines the given *ByteSliceCodecOptions into a single *ByteSliceCodecOptions in a last one wins fashion. +func MergeByteSliceCodecOptions(opts ...*ByteSliceCodecOptions) *ByteSliceCodecOptions { + bs := ByteSliceCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.EncodeNilAsEmpty != nil { + bs.EncodeNilAsEmpty = opt.EncodeNilAsEmpty + } + } + + return bs +} diff --git a/mongo/bson/bsonoptions/doc.go b/mongo/bson/bsonoptions/doc.go new file mode 100644 index 0000000..c40973c --- /dev/null +++ b/mongo/bson/bsonoptions/doc.go @@ -0,0 +1,8 @@ +// Copyright (C) MongoDB, Inc. 2022-present. +// +// 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 + +// Package bsonoptions defines the optional configurations for the BSON codecs. +package bsonoptions diff --git a/mongo/bson/bsonoptions/empty_interface_codec_options.go b/mongo/bson/bsonoptions/empty_interface_codec_options.go new file mode 100644 index 0000000..6caaa00 --- /dev/null +++ b/mongo/bson/bsonoptions/empty_interface_codec_options.go @@ -0,0 +1,38 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// EmptyInterfaceCodecOptions represents all possible options for interface{} encoding and decoding. +type EmptyInterfaceCodecOptions struct { + DecodeBinaryAsSlice *bool // Specifies if Old and Generic type binarys should default to []slice instead of primitive.Binary. Defaults to false. +} + +// EmptyInterfaceCodec creates a new *EmptyInterfaceCodecOptions +func EmptyInterfaceCodec() *EmptyInterfaceCodecOptions { + return &EmptyInterfaceCodecOptions{} +} + +// SetDecodeBinaryAsSlice specifies if Old and Generic type binarys should default to []slice instead of primitive.Binary. Defaults to false. +func (e *EmptyInterfaceCodecOptions) SetDecodeBinaryAsSlice(b bool) *EmptyInterfaceCodecOptions { + e.DecodeBinaryAsSlice = &b + return e +} + +// MergeEmptyInterfaceCodecOptions combines the given *EmptyInterfaceCodecOptions into a single *EmptyInterfaceCodecOptions in a last one wins fashion. +func MergeEmptyInterfaceCodecOptions(opts ...*EmptyInterfaceCodecOptions) *EmptyInterfaceCodecOptions { + e := EmptyInterfaceCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.DecodeBinaryAsSlice != nil { + e.DecodeBinaryAsSlice = opt.DecodeBinaryAsSlice + } + } + + return e +} diff --git a/mongo/bson/bsonoptions/map_codec_options.go b/mongo/bson/bsonoptions/map_codec_options.go new file mode 100644 index 0000000..7a6a880 --- /dev/null +++ b/mongo/bson/bsonoptions/map_codec_options.go @@ -0,0 +1,67 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// MapCodecOptions represents all possible options for map encoding and decoding. +type MapCodecOptions struct { + DecodeZerosMap *bool // Specifies if the map should be zeroed before decoding into it. Defaults to false. + EncodeNilAsEmpty *bool // Specifies if a nil map should encode as an empty document instead of null. Defaults to false. + // Specifies how keys should be handled. If false, the behavior matches encoding/json, where the encoding key type must + // either be a string, an integer type, or implement bsoncodec.KeyMarshaler and the decoding key type must either be a + // string, an integer type, or implement bsoncodec.KeyUnmarshaler. If true, keys are encoded with fmt.Sprint() and the + // encoding key type must be a string, an integer type, or a float. If true, the use of Stringer will override + // TextMarshaler/TextUnmarshaler. Defaults to false. + EncodeKeysWithStringer *bool +} + +// MapCodec creates a new *MapCodecOptions +func MapCodec() *MapCodecOptions { + return &MapCodecOptions{} +} + +// SetDecodeZerosMap specifies if the map should be zeroed before decoding into it. Defaults to false. +func (t *MapCodecOptions) SetDecodeZerosMap(b bool) *MapCodecOptions { + t.DecodeZerosMap = &b + return t +} + +// SetEncodeNilAsEmpty specifies if a nil map should encode as an empty document instead of null. Defaults to false. +func (t *MapCodecOptions) SetEncodeNilAsEmpty(b bool) *MapCodecOptions { + t.EncodeNilAsEmpty = &b + return t +} + +// SetEncodeKeysWithStringer specifies how keys should be handled. If false, the behavior matches encoding/json, where the +// encoding key type must either be a string, an integer type, or implement bsoncodec.KeyMarshaler and the decoding key +// type must either be a string, an integer type, or implement bsoncodec.KeyUnmarshaler. If true, keys are encoded with +// fmt.Sprint() and the encoding key type must be a string, an integer type, or a float. If true, the use of Stringer +// will override TextMarshaler/TextUnmarshaler. Defaults to false. +func (t *MapCodecOptions) SetEncodeKeysWithStringer(b bool) *MapCodecOptions { + t.EncodeKeysWithStringer = &b + return t +} + +// MergeMapCodecOptions combines the given *MapCodecOptions into a single *MapCodecOptions in a last one wins fashion. +func MergeMapCodecOptions(opts ...*MapCodecOptions) *MapCodecOptions { + s := MapCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.DecodeZerosMap != nil { + s.DecodeZerosMap = opt.DecodeZerosMap + } + if opt.EncodeNilAsEmpty != nil { + s.EncodeNilAsEmpty = opt.EncodeNilAsEmpty + } + if opt.EncodeKeysWithStringer != nil { + s.EncodeKeysWithStringer = opt.EncodeKeysWithStringer + } + } + + return s +} diff --git a/mongo/bson/bsonoptions/slice_codec_options.go b/mongo/bson/bsonoptions/slice_codec_options.go new file mode 100644 index 0000000..ef965e4 --- /dev/null +++ b/mongo/bson/bsonoptions/slice_codec_options.go @@ -0,0 +1,38 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// SliceCodecOptions represents all possible options for slice encoding and decoding. +type SliceCodecOptions struct { + EncodeNilAsEmpty *bool // Specifies if a nil slice should encode as an empty array instead of null. Defaults to false. +} + +// SliceCodec creates a new *SliceCodecOptions +func SliceCodec() *SliceCodecOptions { + return &SliceCodecOptions{} +} + +// SetEncodeNilAsEmpty specifies if a nil slice should encode as an empty array instead of null. Defaults to false. +func (s *SliceCodecOptions) SetEncodeNilAsEmpty(b bool) *SliceCodecOptions { + s.EncodeNilAsEmpty = &b + return s +} + +// MergeSliceCodecOptions combines the given *SliceCodecOptions into a single *SliceCodecOptions in a last one wins fashion. +func MergeSliceCodecOptions(opts ...*SliceCodecOptions) *SliceCodecOptions { + s := SliceCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.EncodeNilAsEmpty != nil { + s.EncodeNilAsEmpty = opt.EncodeNilAsEmpty + } + } + + return s +} diff --git a/mongo/bson/bsonoptions/string_codec_options.go b/mongo/bson/bsonoptions/string_codec_options.go new file mode 100644 index 0000000..65964f4 --- /dev/null +++ b/mongo/bson/bsonoptions/string_codec_options.go @@ -0,0 +1,41 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +var defaultDecodeOIDAsHex = true + +// StringCodecOptions represents all possible options for string encoding and decoding. +type StringCodecOptions struct { + DecodeObjectIDAsHex *bool // Specifies if we should decode ObjectID as the hex value. Defaults to true. +} + +// StringCodec creates a new *StringCodecOptions +func StringCodec() *StringCodecOptions { + return &StringCodecOptions{} +} + +// SetDecodeObjectIDAsHex specifies if object IDs should be decoded as their hex representation. If false, a string made +// from the raw object ID bytes will be used. Defaults to true. +func (t *StringCodecOptions) SetDecodeObjectIDAsHex(b bool) *StringCodecOptions { + t.DecodeObjectIDAsHex = &b + return t +} + +// MergeStringCodecOptions combines the given *StringCodecOptions into a single *StringCodecOptions in a last one wins fashion. +func MergeStringCodecOptions(opts ...*StringCodecOptions) *StringCodecOptions { + s := &StringCodecOptions{&defaultDecodeOIDAsHex} + for _, opt := range opts { + if opt == nil { + continue + } + if opt.DecodeObjectIDAsHex != nil { + s.DecodeObjectIDAsHex = opt.DecodeObjectIDAsHex + } + } + + return s +} diff --git a/mongo/bson/bsonoptions/struct_codec_options.go b/mongo/bson/bsonoptions/struct_codec_options.go new file mode 100644 index 0000000..78d1dd8 --- /dev/null +++ b/mongo/bson/bsonoptions/struct_codec_options.go @@ -0,0 +1,87 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +var defaultOverwriteDuplicatedInlinedFields = true + +// StructCodecOptions represents all possible options for struct encoding and decoding. +type StructCodecOptions struct { + DecodeZeroStruct *bool // Specifies if structs should be zeroed before decoding into them. Defaults to false. + DecodeDeepZeroInline *bool // Specifies if structs should be recursively zeroed when a inline value is decoded. Defaults to false. + EncodeOmitDefaultStruct *bool // Specifies if default structs should be considered empty by omitempty. Defaults to false. + AllowUnexportedFields *bool // Specifies if unexported fields should be marshaled/unmarshaled. Defaults to false. + OverwriteDuplicatedInlinedFields *bool // Specifies if fields in inlined structs can be overwritten by higher level struct fields with the same key. Defaults to true. +} + +// StructCodec creates a new *StructCodecOptions +func StructCodec() *StructCodecOptions { + return &StructCodecOptions{} +} + +// SetDecodeZeroStruct specifies if structs should be zeroed before decoding into them. Defaults to false. +func (t *StructCodecOptions) SetDecodeZeroStruct(b bool) *StructCodecOptions { + t.DecodeZeroStruct = &b + return t +} + +// SetDecodeDeepZeroInline specifies if structs should be zeroed before decoding into them. Defaults to false. +func (t *StructCodecOptions) SetDecodeDeepZeroInline(b bool) *StructCodecOptions { + t.DecodeDeepZeroInline = &b + return t +} + +// SetEncodeOmitDefaultStruct specifies if default structs should be considered empty by omitempty. A default struct has all +// its values set to their default value. Defaults to false. +func (t *StructCodecOptions) SetEncodeOmitDefaultStruct(b bool) *StructCodecOptions { + t.EncodeOmitDefaultStruct = &b + return t +} + +// SetOverwriteDuplicatedInlinedFields specifies if inlined struct fields can be overwritten by higher level struct fields with the +// same bson key. When true and decoding, values will be written to the outermost struct with a matching key, and when +// encoding, keys will have the value of the top-most matching field. When false, decoding and encoding will error if +// there are duplicate keys after the struct is inlined. Defaults to true. +func (t *StructCodecOptions) SetOverwriteDuplicatedInlinedFields(b bool) *StructCodecOptions { + t.OverwriteDuplicatedInlinedFields = &b + return t +} + +// SetAllowUnexportedFields specifies if unexported fields should be marshaled/unmarshaled. Defaults to false. +func (t *StructCodecOptions) SetAllowUnexportedFields(b bool) *StructCodecOptions { + t.AllowUnexportedFields = &b + return t +} + +// MergeStructCodecOptions combines the given *StructCodecOptions into a single *StructCodecOptions in a last one wins fashion. +func MergeStructCodecOptions(opts ...*StructCodecOptions) *StructCodecOptions { + s := &StructCodecOptions{ + OverwriteDuplicatedInlinedFields: &defaultOverwriteDuplicatedInlinedFields, + } + for _, opt := range opts { + if opt == nil { + continue + } + + if opt.DecodeZeroStruct != nil { + s.DecodeZeroStruct = opt.DecodeZeroStruct + } + if opt.DecodeDeepZeroInline != nil { + s.DecodeDeepZeroInline = opt.DecodeDeepZeroInline + } + if opt.EncodeOmitDefaultStruct != nil { + s.EncodeOmitDefaultStruct = opt.EncodeOmitDefaultStruct + } + if opt.OverwriteDuplicatedInlinedFields != nil { + s.OverwriteDuplicatedInlinedFields = opt.OverwriteDuplicatedInlinedFields + } + if opt.AllowUnexportedFields != nil { + s.AllowUnexportedFields = opt.AllowUnexportedFields + } + } + + return s +} diff --git a/mongo/bson/bsonoptions/time_codec_options.go b/mongo/bson/bsonoptions/time_codec_options.go new file mode 100644 index 0000000..13496d1 --- /dev/null +++ b/mongo/bson/bsonoptions/time_codec_options.go @@ -0,0 +1,38 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// TimeCodecOptions represents all possible options for time.Time encoding and decoding. +type TimeCodecOptions struct { + UseLocalTimeZone *bool // Specifies if we should decode into the local time zone. Defaults to false. +} + +// TimeCodec creates a new *TimeCodecOptions +func TimeCodec() *TimeCodecOptions { + return &TimeCodecOptions{} +} + +// SetUseLocalTimeZone specifies if we should decode into the local time zone. Defaults to false. +func (t *TimeCodecOptions) SetUseLocalTimeZone(b bool) *TimeCodecOptions { + t.UseLocalTimeZone = &b + return t +} + +// MergeTimeCodecOptions combines the given *TimeCodecOptions into a single *TimeCodecOptions in a last one wins fashion. +func MergeTimeCodecOptions(opts ...*TimeCodecOptions) *TimeCodecOptions { + t := TimeCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.UseLocalTimeZone != nil { + t.UseLocalTimeZone = opt.UseLocalTimeZone + } + } + + return t +} diff --git a/mongo/bson/bsonoptions/uint_codec_options.go b/mongo/bson/bsonoptions/uint_codec_options.go new file mode 100644 index 0000000..e08b7f1 --- /dev/null +++ b/mongo/bson/bsonoptions/uint_codec_options.go @@ -0,0 +1,38 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonoptions + +// UIntCodecOptions represents all possible options for uint encoding and decoding. +type UIntCodecOptions struct { + EncodeToMinSize *bool // Specifies if all uints except uint64 should be decoded to minimum size bsontype. Defaults to false. +} + +// UIntCodec creates a new *UIntCodecOptions +func UIntCodec() *UIntCodecOptions { + return &UIntCodecOptions{} +} + +// SetEncodeToMinSize specifies if all uints except uint64 should be decoded to minimum size bsontype. Defaults to false. +func (u *UIntCodecOptions) SetEncodeToMinSize(b bool) *UIntCodecOptions { + u.EncodeToMinSize = &b + return u +} + +// MergeUIntCodecOptions combines the given *UIntCodecOptions into a single *UIntCodecOptions in a last one wins fashion. +func MergeUIntCodecOptions(opts ...*UIntCodecOptions) *UIntCodecOptions { + u := UIntCodec() + for _, opt := range opts { + if opt == nil { + continue + } + if opt.EncodeToMinSize != nil { + u.EncodeToMinSize = opt.EncodeToMinSize + } + } + + return u +} diff --git a/mongo/bson/bsonrw/bsonrw_test.go b/mongo/bson/bsonrw/bsonrw_test.go new file mode 100644 index 0000000..4657961 --- /dev/null +++ b/mongo/bson/bsonrw/bsonrw_test.go @@ -0,0 +1,33 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import "testing" + +func compareErrors(err1, err2 error) bool { + if err1 == nil && err2 == nil { + return true + } + + if err1 == nil || err2 == nil { + return false + } + + if err1.Error() != err2.Error() { + return false + } + + return true +} + +func noerr(t *testing.T, err error) { + if err != nil { + t.Helper() + t.Errorf("Unexpected error: (%T)%v", err, err) + t.FailNow() + } +} diff --git a/mongo/bson/bsonrw/bsonrwtest/bsonrwtest.go b/mongo/bson/bsonrw/bsonrwtest/bsonrwtest.go new file mode 100644 index 0000000..355c4b3 --- /dev/null +++ b/mongo/bson/bsonrw/bsonrwtest/bsonrwtest.go @@ -0,0 +1,848 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +// Package bsonrwtest provides utilities for testing the "bson/bsonrw" package. +package bsonrwtest // import "go.mongodb.org/mongo-driver/bson/bsonrw/bsonrwtest" + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +var _ bsonrw.ValueReader = (*ValueReaderWriter)(nil) +var _ bsonrw.ValueWriter = (*ValueReaderWriter)(nil) + +// Invoked is a type used to indicate what method was called last. +type Invoked byte + +// These are the different methods that can be invoked. +const ( + Nothing Invoked = iota + ReadArray + ReadBinary + ReadBoolean + ReadDocument + ReadCodeWithScope + ReadDBPointer + ReadDateTime + ReadDecimal128 + ReadDouble + ReadInt32 + ReadInt64 + ReadJavascript + ReadMaxKey + ReadMinKey + ReadNull + ReadObjectID + ReadRegex + ReadString + ReadSymbol + ReadTimestamp + ReadUndefined + ReadElement + ReadValue + WriteArray + WriteBinary + WriteBinaryWithSubtype + WriteBoolean + WriteCodeWithScope + WriteDBPointer + WriteDateTime + WriteDecimal128 + WriteDouble + WriteInt32 + WriteInt64 + WriteJavascript + WriteMaxKey + WriteMinKey + WriteNull + WriteObjectID + WriteRegex + WriteString + WriteDocument + WriteSymbol + WriteTimestamp + WriteUndefined + WriteDocumentElement + WriteDocumentEnd + WriteArrayElement + WriteArrayEnd + Skip +) + +func (i Invoked) String() string { + switch i { + case Nothing: + return "Nothing" + case ReadArray: + return "ReadArray" + case ReadBinary: + return "ReadBinary" + case ReadBoolean: + return "ReadBoolean" + case ReadDocument: + return "ReadDocument" + case ReadCodeWithScope: + return "ReadCodeWithScope" + case ReadDBPointer: + return "ReadDBPointer" + case ReadDateTime: + return "ReadDateTime" + case ReadDecimal128: + return "ReadDecimal128" + case ReadDouble: + return "ReadDouble" + case ReadInt32: + return "ReadInt32" + case ReadInt64: + return "ReadInt64" + case ReadJavascript: + return "ReadJavascript" + case ReadMaxKey: + return "ReadMaxKey" + case ReadMinKey: + return "ReadMinKey" + case ReadNull: + return "ReadNull" + case ReadObjectID: + return "ReadObjectID" + case ReadRegex: + return "ReadRegex" + case ReadString: + return "ReadString" + case ReadSymbol: + return "ReadSymbol" + case ReadTimestamp: + return "ReadTimestamp" + case ReadUndefined: + return "ReadUndefined" + case ReadElement: + return "ReadElement" + case ReadValue: + return "ReadValue" + case WriteArray: + return "WriteArray" + case WriteBinary: + return "WriteBinary" + case WriteBinaryWithSubtype: + return "WriteBinaryWithSubtype" + case WriteBoolean: + return "WriteBoolean" + case WriteCodeWithScope: + return "WriteCodeWithScope" + case WriteDBPointer: + return "WriteDBPointer" + case WriteDateTime: + return "WriteDateTime" + case WriteDecimal128: + return "WriteDecimal128" + case WriteDouble: + return "WriteDouble" + case WriteInt32: + return "WriteInt32" + case WriteInt64: + return "WriteInt64" + case WriteJavascript: + return "WriteJavascript" + case WriteMaxKey: + return "WriteMaxKey" + case WriteMinKey: + return "WriteMinKey" + case WriteNull: + return "WriteNull" + case WriteObjectID: + return "WriteObjectID" + case WriteRegex: + return "WriteRegex" + case WriteString: + return "WriteString" + case WriteDocument: + return "WriteDocument" + case WriteSymbol: + return "WriteSymbol" + case WriteTimestamp: + return "WriteTimestamp" + case WriteUndefined: + return "WriteUndefined" + case WriteDocumentElement: + return "WriteDocumentElement" + case WriteDocumentEnd: + return "WriteDocumentEnd" + case WriteArrayElement: + return "WriteArrayElement" + case WriteArrayEnd: + return "WriteArrayEnd" + default: + return "" + } +} + +// ValueReaderWriter is a test implementation of a bsonrw.ValueReader and bsonrw.ValueWriter +type ValueReaderWriter struct { + T *testing.T + Invoked Invoked + Return interface{} // Can be a primitive or a bsoncore.Value + BSONType bsontype.Type + Err error + ErrAfter Invoked // error after this method is called + depth uint64 +} + +// prevent infinite recursion. +func (llvrw *ValueReaderWriter) checkdepth() { + llvrw.depth++ + if llvrw.depth > 1000 { + panic("max depth exceeded") + } +} + +// Type implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) Type() bsontype.Type { + llvrw.checkdepth() + return llvrw.BSONType +} + +// Skip implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) Skip() error { + llvrw.checkdepth() + llvrw.Invoked = Skip + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// ReadArray implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadArray() (bsonrw.ArrayReader, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadArray + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + + return llvrw, nil +} + +// ReadBinary implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadBinary() (b []byte, btype byte, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadBinary + if llvrw.ErrAfter == llvrw.Invoked { + return nil, 0x00, llvrw.Err + } + + switch tt := llvrw.Return.(type) { + case bsoncore.Value: + subtype, data, _, ok := bsoncore.ReadBinary(tt.Data) + if !ok { + llvrw.T.Error("Invalid Value provided for return value of ReadBinary.") + return nil, 0x00, nil + } + return data, subtype, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadBinary: %T", llvrw.Return) + return nil, 0x00, nil + } +} + +// ReadBoolean implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadBoolean() (bool, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadBoolean + if llvrw.ErrAfter == llvrw.Invoked { + return false, llvrw.Err + } + + switch tt := llvrw.Return.(type) { + case bool: + return tt, nil + case bsoncore.Value: + b, _, ok := bsoncore.ReadBoolean(tt.Data) + if !ok { + llvrw.T.Error("Invalid Value provided for return value of ReadBoolean.") + return false, nil + } + return b, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadBoolean: %T", llvrw.Return) + return false, nil + } +} + +// ReadDocument implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadDocument() (bsonrw.DocumentReader, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadDocument + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + + return llvrw, nil +} + +// ReadCodeWithScope implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadCodeWithScope() (code string, dr bsonrw.DocumentReader, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadCodeWithScope + if llvrw.ErrAfter == llvrw.Invoked { + return "", nil, llvrw.Err + } + + return "", llvrw, nil +} + +// ReadDBPointer implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadDBPointer() (ns string, oid primitive.ObjectID, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadDBPointer + if llvrw.ErrAfter == llvrw.Invoked { + return "", primitive.ObjectID{}, llvrw.Err + } + + switch tt := llvrw.Return.(type) { + case bsoncore.Value: + ns, oid, _, ok := bsoncore.ReadDBPointer(tt.Data) + if !ok { + llvrw.T.Error("Invalid Value instance provided for return value of ReadDBPointer") + return "", primitive.ObjectID{}, nil + } + return ns, oid, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadDBPointer: %T", llvrw.Return) + return "", primitive.ObjectID{}, nil + } +} + +// ReadDateTime implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadDateTime() (int64, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadDateTime + if llvrw.ErrAfter == llvrw.Invoked { + return 0, llvrw.Err + } + + dt, ok := llvrw.Return.(int64) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadDateTime: %T", llvrw.Return) + return 0, nil + } + + return dt, nil +} + +// ReadDecimal128 implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadDecimal128() (primitive.Decimal128, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadDecimal128 + if llvrw.ErrAfter == llvrw.Invoked { + return primitive.Decimal128{}, llvrw.Err + } + + d128, ok := llvrw.Return.(primitive.Decimal128) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadDecimal128: %T", llvrw.Return) + return primitive.Decimal128{}, nil + } + + return d128, nil +} + +// ReadDouble implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadDouble() (float64, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadDouble + if llvrw.ErrAfter == llvrw.Invoked { + return 0, llvrw.Err + } + + f64, ok := llvrw.Return.(float64) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadDouble: %T", llvrw.Return) + return 0, nil + } + + return f64, nil +} + +// ReadInt32 implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadInt32() (int32, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadInt32 + if llvrw.ErrAfter == llvrw.Invoked { + return 0, llvrw.Err + } + + i32, ok := llvrw.Return.(int32) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadInt32: %T", llvrw.Return) + return 0, nil + } + + return i32, nil +} + +// ReadInt64 implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadInt64() (int64, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadInt64 + if llvrw.ErrAfter == llvrw.Invoked { + return 0, llvrw.Err + } + i64, ok := llvrw.Return.(int64) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadInt64: %T", llvrw.Return) + return 0, nil + } + + return i64, nil +} + +// ReadJavascript implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadJavascript() (code string, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadJavascript + if llvrw.ErrAfter == llvrw.Invoked { + return "", llvrw.Err + } + js, ok := llvrw.Return.(string) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadJavascript: %T", llvrw.Return) + return "", nil + } + + return js, nil +} + +// ReadMaxKey implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadMaxKey() error { + llvrw.checkdepth() + llvrw.Invoked = ReadMaxKey + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} + +// ReadMinKey implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadMinKey() error { + llvrw.checkdepth() + llvrw.Invoked = ReadMinKey + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} + +// ReadNull implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadNull() error { + llvrw.checkdepth() + llvrw.Invoked = ReadNull + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} + +// ReadObjectID implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadObjectID() (primitive.ObjectID, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadObjectID + if llvrw.ErrAfter == llvrw.Invoked { + return primitive.ObjectID{}, llvrw.Err + } + oid, ok := llvrw.Return.(primitive.ObjectID) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadObjectID: %T", llvrw.Return) + return primitive.ObjectID{}, nil + } + + return oid, nil +} + +// ReadRegex implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadRegex() (pattern string, options string, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadRegex + if llvrw.ErrAfter == llvrw.Invoked { + return "", "", llvrw.Err + } + switch tt := llvrw.Return.(type) { + case bsoncore.Value: + pattern, options, _, ok := bsoncore.ReadRegex(tt.Data) + if !ok { + llvrw.T.Error("Invalid Value instance provided for ReadRegex") + return "", "", nil + } + return pattern, options, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadRegex: %T", llvrw.Return) + return "", "", nil + } +} + +// ReadString implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadString() (string, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadString + if llvrw.ErrAfter == llvrw.Invoked { + return "", llvrw.Err + } + str, ok := llvrw.Return.(string) + if !ok { + llvrw.T.Errorf("Incorrect type provided for return value of ReadString: %T", llvrw.Return) + return "", nil + } + + return str, nil +} + +// ReadSymbol implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadSymbol() (symbol string, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadSymbol + if llvrw.ErrAfter == llvrw.Invoked { + return "", llvrw.Err + } + switch tt := llvrw.Return.(type) { + case string: + return tt, nil + case bsoncore.Value: + symbol, _, ok := bsoncore.ReadSymbol(tt.Data) + if !ok { + llvrw.T.Error("Invalid Value instance provided for ReadSymbol") + return "", nil + } + return symbol, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadSymbol: %T", llvrw.Return) + return "", nil + } +} + +// ReadTimestamp implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadTimestamp() (t uint32, i uint32, err error) { + llvrw.checkdepth() + llvrw.Invoked = ReadTimestamp + if llvrw.ErrAfter == llvrw.Invoked { + return 0, 0, llvrw.Err + } + switch tt := llvrw.Return.(type) { + case bsoncore.Value: + t, i, _, ok := bsoncore.ReadTimestamp(tt.Data) + if !ok { + llvrw.T.Errorf("Invalid Value instance provided for return value of ReadTimestamp") + return 0, 0, nil + } + return t, i, nil + default: + llvrw.T.Errorf("Incorrect type provided for return value of ReadTimestamp: %T", llvrw.Return) + return 0, 0, nil + } +} + +// ReadUndefined implements the bsonrw.ValueReader interface. +func (llvrw *ValueReaderWriter) ReadUndefined() error { + llvrw.checkdepth() + llvrw.Invoked = ReadUndefined + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} + +// WriteArray implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteArray() (bsonrw.ArrayWriter, error) { + llvrw.checkdepth() + llvrw.Invoked = WriteArray + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + return llvrw, nil +} + +// WriteBinary implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteBinary(b []byte) error { + llvrw.checkdepth() + llvrw.Invoked = WriteBinary + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteBinaryWithSubtype implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteBinaryWithSubtype(b []byte, btype byte) error { + llvrw.checkdepth() + llvrw.Invoked = WriteBinaryWithSubtype + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteBoolean implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteBoolean(bool) error { + llvrw.checkdepth() + llvrw.Invoked = WriteBoolean + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteCodeWithScope implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteCodeWithScope(code string) (bsonrw.DocumentWriter, error) { + llvrw.checkdepth() + llvrw.Invoked = WriteCodeWithScope + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + return llvrw, nil +} + +// WriteDBPointer implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteDBPointer(ns string, oid primitive.ObjectID) error { + llvrw.checkdepth() + llvrw.Invoked = WriteDBPointer + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteDateTime implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteDateTime(dt int64) error { + llvrw.checkdepth() + llvrw.Invoked = WriteDateTime + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteDecimal128 implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteDecimal128(primitive.Decimal128) error { + llvrw.checkdepth() + llvrw.Invoked = WriteDecimal128 + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteDouble implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteDouble(float64) error { + llvrw.checkdepth() + llvrw.Invoked = WriteDouble + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteInt32 implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteInt32(int32) error { + llvrw.checkdepth() + llvrw.Invoked = WriteInt32 + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteInt64 implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteInt64(int64) error { + llvrw.checkdepth() + llvrw.Invoked = WriteInt64 + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteJavascript implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteJavascript(code string) error { + llvrw.checkdepth() + llvrw.Invoked = WriteJavascript + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteMaxKey implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteMaxKey() error { + llvrw.checkdepth() + llvrw.Invoked = WriteMaxKey + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteMinKey implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteMinKey() error { + llvrw.checkdepth() + llvrw.Invoked = WriteMinKey + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteNull implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteNull() error { + llvrw.checkdepth() + llvrw.Invoked = WriteNull + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteObjectID implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteObjectID(primitive.ObjectID) error { + llvrw.checkdepth() + llvrw.Invoked = WriteObjectID + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteRegex implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteRegex(pattern string, options string) error { + llvrw.checkdepth() + llvrw.Invoked = WriteRegex + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteString implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteString(string) error { + llvrw.checkdepth() + llvrw.Invoked = WriteString + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteDocument implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteDocument() (bsonrw.DocumentWriter, error) { + llvrw.checkdepth() + llvrw.Invoked = WriteDocument + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + return llvrw, nil +} + +// WriteSymbol implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteSymbol(symbol string) error { + llvrw.checkdepth() + llvrw.Invoked = WriteSymbol + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteTimestamp implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteTimestamp(t uint32, i uint32) error { + llvrw.checkdepth() + llvrw.Invoked = WriteTimestamp + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// WriteUndefined implements the bsonrw.ValueWriter interface. +func (llvrw *ValueReaderWriter) WriteUndefined() error { + llvrw.checkdepth() + llvrw.Invoked = WriteUndefined + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + return nil +} + +// ReadElement implements the bsonrw.DocumentReader interface. +func (llvrw *ValueReaderWriter) ReadElement() (string, bsonrw.ValueReader, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadElement + if llvrw.ErrAfter == llvrw.Invoked { + return "", nil, llvrw.Err + } + + return "", llvrw, nil +} + +// WriteDocumentElement implements the bsonrw.DocumentWriter interface. +func (llvrw *ValueReaderWriter) WriteDocumentElement(string) (bsonrw.ValueWriter, error) { + llvrw.checkdepth() + llvrw.Invoked = WriteDocumentElement + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + + return llvrw, nil +} + +// WriteDocumentEnd implements the bsonrw.DocumentWriter interface. +func (llvrw *ValueReaderWriter) WriteDocumentEnd() error { + llvrw.checkdepth() + llvrw.Invoked = WriteDocumentEnd + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} + +// ReadValue implements the bsonrw.ArrayReader interface. +func (llvrw *ValueReaderWriter) ReadValue() (bsonrw.ValueReader, error) { + llvrw.checkdepth() + llvrw.Invoked = ReadValue + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + + return llvrw, nil +} + +// WriteArrayElement implements the bsonrw.ArrayWriter interface. +func (llvrw *ValueReaderWriter) WriteArrayElement() (bsonrw.ValueWriter, error) { + llvrw.checkdepth() + llvrw.Invoked = WriteArrayElement + if llvrw.ErrAfter == llvrw.Invoked { + return nil, llvrw.Err + } + + return llvrw, nil +} + +// WriteArrayEnd implements the bsonrw.ArrayWriter interface. +func (llvrw *ValueReaderWriter) WriteArrayEnd() error { + llvrw.checkdepth() + llvrw.Invoked = WriteArrayEnd + if llvrw.ErrAfter == llvrw.Invoked { + return llvrw.Err + } + + return nil +} diff --git a/mongo/bson/bsonrw/copier.go b/mongo/bson/bsonrw/copier.go new file mode 100644 index 0000000..5cdf646 --- /dev/null +++ b/mongo/bson/bsonrw/copier.go @@ -0,0 +1,445 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "fmt" + "io" + + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +// Copier is a type that allows copying between ValueReaders, ValueWriters, and +// []byte values. +type Copier struct{} + +// NewCopier creates a new copier with the given registry. If a nil registry is provided +// a default registry is used. +func NewCopier() Copier { + return Copier{} +} + +// CopyDocument handles copying a document from src to dst. +func CopyDocument(dst ValueWriter, src ValueReader) error { + return Copier{}.CopyDocument(dst, src) +} + +// CopyDocument handles copying one document from the src to the dst. +func (c Copier) CopyDocument(dst ValueWriter, src ValueReader) error { + dr, err := src.ReadDocument() + if err != nil { + return err + } + + dw, err := dst.WriteDocument() + if err != nil { + return err + } + + return c.copyDocumentCore(dw, dr) +} + +// CopyArrayFromBytes copies the values from a BSON array represented as a +// []byte to a ValueWriter. +func (c Copier) CopyArrayFromBytes(dst ValueWriter, src []byte) error { + aw, err := dst.WriteArray() + if err != nil { + return err + } + + err = c.CopyBytesToArrayWriter(aw, src) + if err != nil { + return err + } + + return aw.WriteArrayEnd() +} + +// CopyDocumentFromBytes copies the values from a BSON document represented as a +// []byte to a ValueWriter. +func (c Copier) CopyDocumentFromBytes(dst ValueWriter, src []byte) error { + dw, err := dst.WriteDocument() + if err != nil { + return err + } + + err = c.CopyBytesToDocumentWriter(dw, src) + if err != nil { + return err + } + + return dw.WriteDocumentEnd() +} + +type writeElementFn func(key string) (ValueWriter, error) + +// CopyBytesToArrayWriter copies the values from a BSON Array represented as a []byte to an +// ArrayWriter. +func (c Copier) CopyBytesToArrayWriter(dst ArrayWriter, src []byte) error { + wef := func(_ string) (ValueWriter, error) { + return dst.WriteArrayElement() + } + + return c.copyBytesToValueWriter(src, wef) +} + +// CopyBytesToDocumentWriter copies the values from a BSON document represented as a []byte to a +// DocumentWriter. +func (c Copier) CopyBytesToDocumentWriter(dst DocumentWriter, src []byte) error { + wef := func(key string) (ValueWriter, error) { + return dst.WriteDocumentElement(key) + } + + return c.copyBytesToValueWriter(src, wef) +} + +func (c Copier) copyBytesToValueWriter(src []byte, wef writeElementFn) error { + // TODO(skriptble): Create errors types here. Anything thats a tag should be a property. + length, rem, ok := bsoncore.ReadLength(src) + if !ok { + return fmt.Errorf("couldn't read length from src, not enough bytes. length=%d", len(src)) + } + if len(src) < int(length) { + return fmt.Errorf("length read exceeds number of bytes available. length=%d bytes=%d", len(src), length) + } + rem = rem[:length-4] + + var t bsontype.Type + var key string + var val bsoncore.Value + for { + t, rem, ok = bsoncore.ReadType(rem) + if !ok { + return io.EOF + } + if t == bsontype.Type(0) { + if len(rem) != 0 { + return fmt.Errorf("document end byte found before end of document. remaining bytes=%v", rem) + } + break + } + + key, rem, ok = bsoncore.ReadKey(rem) + if !ok { + return fmt.Errorf("invalid key found. remaining bytes=%v", rem) + } + + // write as either array element or document element using writeElementFn + vw, err := wef(key) + if err != nil { + return err + } + + val, rem, ok = bsoncore.ReadValue(rem, t) + if !ok { + return fmt.Errorf("not enough bytes available to read type. bytes=%d type=%s", len(rem), t) + } + err = c.CopyValueFromBytes(vw, t, val.Data) + if err != nil { + return err + } + } + return nil +} + +// CopyDocumentToBytes copies an entire document from the ValueReader and +// returns it as bytes. +func (c Copier) CopyDocumentToBytes(src ValueReader) ([]byte, error) { + return c.AppendDocumentBytes(nil, src) +} + +// AppendDocumentBytes functions the same as CopyDocumentToBytes, but will +// append the result to dst. +func (c Copier) AppendDocumentBytes(dst []byte, src ValueReader) ([]byte, error) { + if br, ok := src.(BytesReader); ok { + _, dst, err := br.ReadValueBytes(dst) + return dst, err + } + + vw := vwPool.Get().(*valueWriter) + defer vwPool.Put(vw) + + vw.reset(dst) + + err := c.CopyDocument(vw, src) + dst = vw.buf + return dst, err +} + +// AppendArrayBytes copies an array from the ValueReader to dst. +func (c Copier) AppendArrayBytes(dst []byte, src ValueReader) ([]byte, error) { + if br, ok := src.(BytesReader); ok { + _, dst, err := br.ReadValueBytes(dst) + return dst, err + } + + vw := vwPool.Get().(*valueWriter) + defer vwPool.Put(vw) + + vw.reset(dst) + + err := c.copyArray(vw, src) + dst = vw.buf + return dst, err +} + +// CopyValueFromBytes will write the value represtend by t and src to dst. +func (c Copier) CopyValueFromBytes(dst ValueWriter, t bsontype.Type, src []byte) error { + if wvb, ok := dst.(BytesWriter); ok { + return wvb.WriteValueBytes(t, src) + } + + vr := vrPool.Get().(*valueReader) + defer vrPool.Put(vr) + + vr.reset(src) + vr.pushElement(t) + + return c.CopyValue(dst, vr) +} + +// CopyValueToBytes copies a value from src and returns it as a bsontype.Type and a +// []byte. +func (c Copier) CopyValueToBytes(src ValueReader) (bsontype.Type, []byte, error) { + return c.AppendValueBytes(nil, src) +} + +// AppendValueBytes functions the same as CopyValueToBytes, but will append the +// result to dst. +func (c Copier) AppendValueBytes(dst []byte, src ValueReader) (bsontype.Type, []byte, error) { + if br, ok := src.(BytesReader); ok { + return br.ReadValueBytes(dst) + } + + vw := vwPool.Get().(*valueWriter) + defer vwPool.Put(vw) + + start := len(dst) + + vw.reset(dst) + vw.push(mElement) + + err := c.CopyValue(vw, src) + if err != nil { + return 0, dst, err + } + + return bsontype.Type(vw.buf[start]), vw.buf[start+2:], nil +} + +// CopyValue will copy a single value from src to dst. +func (c Copier) CopyValue(dst ValueWriter, src ValueReader) error { + var err error + switch src.Type() { + case bsontype.Double: + var f64 float64 + f64, err = src.ReadDouble() + if err != nil { + break + } + err = dst.WriteDouble(f64) + case bsontype.String: + var str string + str, err = src.ReadString() + if err != nil { + return err + } + err = dst.WriteString(str) + case bsontype.EmbeddedDocument: + err = c.CopyDocument(dst, src) + case bsontype.Array: + err = c.copyArray(dst, src) + case bsontype.Binary: + var data []byte + var subtype byte + data, subtype, err = src.ReadBinary() + if err != nil { + break + } + err = dst.WriteBinaryWithSubtype(data, subtype) + case bsontype.Undefined: + err = src.ReadUndefined() + if err != nil { + break + } + err = dst.WriteUndefined() + case bsontype.ObjectID: + var oid primitive.ObjectID + oid, err = src.ReadObjectID() + if err != nil { + break + } + err = dst.WriteObjectID(oid) + case bsontype.Boolean: + var b bool + b, err = src.ReadBoolean() + if err != nil { + break + } + err = dst.WriteBoolean(b) + case bsontype.DateTime: + var dt int64 + dt, err = src.ReadDateTime() + if err != nil { + break + } + err = dst.WriteDateTime(dt) + case bsontype.Null: + err = src.ReadNull() + if err != nil { + break + } + err = dst.WriteNull() + case bsontype.Regex: + var pattern, options string + pattern, options, err = src.ReadRegex() + if err != nil { + break + } + err = dst.WriteRegex(pattern, options) + case bsontype.DBPointer: + var ns string + var pointer primitive.ObjectID + ns, pointer, err = src.ReadDBPointer() + if err != nil { + break + } + err = dst.WriteDBPointer(ns, pointer) + case bsontype.JavaScript: + var js string + js, err = src.ReadJavascript() + if err != nil { + break + } + err = dst.WriteJavascript(js) + case bsontype.Symbol: + var symbol string + symbol, err = src.ReadSymbol() + if err != nil { + break + } + err = dst.WriteSymbol(symbol) + case bsontype.CodeWithScope: + var code string + var srcScope DocumentReader + code, srcScope, err = src.ReadCodeWithScope() + if err != nil { + break + } + + var dstScope DocumentWriter + dstScope, err = dst.WriteCodeWithScope(code) + if err != nil { + break + } + err = c.copyDocumentCore(dstScope, srcScope) + case bsontype.Int32: + var i32 int32 + i32, err = src.ReadInt32() + if err != nil { + break + } + err = dst.WriteInt32(i32) + case bsontype.Timestamp: + var t, i uint32 + t, i, err = src.ReadTimestamp() + if err != nil { + break + } + err = dst.WriteTimestamp(t, i) + case bsontype.Int64: + var i64 int64 + i64, err = src.ReadInt64() + if err != nil { + break + } + err = dst.WriteInt64(i64) + case bsontype.Decimal128: + var d128 primitive.Decimal128 + d128, err = src.ReadDecimal128() + if err != nil { + break + } + err = dst.WriteDecimal128(d128) + case bsontype.MinKey: + err = src.ReadMinKey() + if err != nil { + break + } + err = dst.WriteMinKey() + case bsontype.MaxKey: + err = src.ReadMaxKey() + if err != nil { + break + } + err = dst.WriteMaxKey() + default: + err = fmt.Errorf("Cannot copy unknown BSON type %s", src.Type()) + } + + return err +} + +func (c Copier) copyArray(dst ValueWriter, src ValueReader) error { + ar, err := src.ReadArray() + if err != nil { + return err + } + + aw, err := dst.WriteArray() + if err != nil { + return err + } + + for { + vr, err := ar.ReadValue() + if err == ErrEOA { + break + } + if err != nil { + return err + } + + vw, err := aw.WriteArrayElement() + if err != nil { + return err + } + + err = c.CopyValue(vw, vr) + if err != nil { + return err + } + } + + return aw.WriteArrayEnd() +} + +func (c Copier) copyDocumentCore(dw DocumentWriter, dr DocumentReader) error { + for { + key, vr, err := dr.ReadElement() + if err == ErrEOD { + break + } + if err != nil { + return err + } + + vw, err := dw.WriteDocumentElement(key) + if err != nil { + return err + } + + err = c.CopyValue(vw, vr) + if err != nil { + return err + } + } + + return dw.WriteDocumentEnd() +} diff --git a/mongo/bson/bsonrw/copier_test.go b/mongo/bson/bsonrw/copier_test.go new file mode 100644 index 0000000..c57235c --- /dev/null +++ b/mongo/bson/bsonrw/copier_test.go @@ -0,0 +1,529 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" +) + +func TestCopier(t *testing.T) { + t.Run("CopyDocument", func(t *testing.T) { + t.Run("ReadDocument Error", func(t *testing.T) { + want := errors.New("ReadDocumentError") + src := &TestValueReaderWriter{t: t, err: want, errAfter: llvrwReadDocument} + got := Copier{}.CopyDocument(nil, src) + if !compareErrors(got, want) { + t.Errorf("Did not receive correct error. got %v; want %v", got, want) + } + }) + t.Run("WriteDocument Error", func(t *testing.T) { + want := errors.New("WriteDocumentError") + src := &TestValueReaderWriter{} + dst := &TestValueReaderWriter{t: t, err: want, errAfter: llvrwWriteDocument} + got := Copier{}.CopyDocument(dst, src) + if !compareErrors(got, want) { + t.Errorf("Did not receive correct error. got %v; want %v", got, want) + } + }) + t.Run("success", func(t *testing.T) { + idx, doc := bsoncore.AppendDocumentStart(nil) + doc = bsoncore.AppendStringElement(doc, "Hello", "world") + doc, err := bsoncore.AppendDocumentEnd(doc, idx) + noerr(t, err) + src := newValueReader(doc) + dst := newValueWriterFromSlice(make([]byte, 0)) + want := doc + err = Copier{}.CopyDocument(dst, src) + noerr(t, err) + got := dst.buf + if !bytes.Equal(got, want) { + t.Errorf("Bytes are not equal. got %v; want %v", got, want) + } + }) + }) + t.Run("copyArray", func(t *testing.T) { + t.Run("ReadArray Error", func(t *testing.T) { + want := errors.New("ReadArrayError") + src := &TestValueReaderWriter{t: t, err: want, errAfter: llvrwReadArray} + got := Copier{}.copyArray(nil, src) + if !compareErrors(got, want) { + t.Errorf("Did not receive correct error. got %v; want %v", got, want) + } + }) + t.Run("WriteArray Error", func(t *testing.T) { + want := errors.New("WriteArrayError") + src := &TestValueReaderWriter{} + dst := &TestValueReaderWriter{t: t, err: want, errAfter: llvrwWriteArray} + got := Copier{}.copyArray(dst, src) + if !compareErrors(got, want) { + t.Errorf("Did not receive correct error. got %v; want %v", got, want) + } + }) + t.Run("success", func(t *testing.T) { + idx, doc := bsoncore.AppendDocumentStart(nil) + aidx, doc := bsoncore.AppendArrayElementStart(doc, "foo") + doc = bsoncore.AppendStringElement(doc, "0", "Hello, world!") + doc, err := bsoncore.AppendArrayEnd(doc, aidx) + noerr(t, err) + doc, err = bsoncore.AppendDocumentEnd(doc, idx) + noerr(t, err) + src := newValueReader(doc) + + _, err = src.ReadDocument() + noerr(t, err) + _, _, err = src.ReadElement() + noerr(t, err) + + dst := newValueWriterFromSlice(make([]byte, 0)) + _, err = dst.WriteDocument() + noerr(t, err) + _, err = dst.WriteDocumentElement("foo") + noerr(t, err) + want := doc + + err = Copier{}.copyArray(dst, src) + noerr(t, err) + + err = dst.WriteDocumentEnd() + noerr(t, err) + + got := dst.buf + if !bytes.Equal(got, want) { + t.Errorf("Bytes are not equal. got %v; want %v", got, want) + } + }) + }) + t.Run("CopyValue", func(t *testing.T) { + testCases := []struct { + name string + dst *TestValueReaderWriter + src *TestValueReaderWriter + err error + }{ + { + "Double/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Double, err: errors.New("1"), errAfter: llvrwReadDouble}, + errors.New("1"), + }, + { + "Double/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Double, err: errors.New("2"), errAfter: llvrwWriteDouble}, + &TestValueReaderWriter{bsontype: bsontype.Double, readval: float64(3.14159)}, + errors.New("2"), + }, + { + "String/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.String, err: errors.New("1"), errAfter: llvrwReadString}, + errors.New("1"), + }, + { + "String/dst/error", + &TestValueReaderWriter{bsontype: bsontype.String, err: errors.New("2"), errAfter: llvrwWriteString}, + &TestValueReaderWriter{bsontype: bsontype.String, readval: "hello, world"}, + errors.New("2"), + }, + { + "Document/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.EmbeddedDocument, err: errors.New("1"), errAfter: llvrwReadDocument}, + errors.New("1"), + }, + { + "Array/dst/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Array, err: errors.New("2"), errAfter: llvrwReadArray}, + errors.New("2"), + }, + { + "Binary/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Binary, err: errors.New("1"), errAfter: llvrwReadBinary}, + errors.New("1"), + }, + { + "Binary/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Binary, err: errors.New("2"), errAfter: llvrwWriteBinaryWithSubtype}, + &TestValueReaderWriter{ + bsontype: bsontype.Binary, + readval: bsoncore.Value{ + Type: bsontype.Binary, + Data: []byte{0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x02, 0x03}, + }, + }, + errors.New("2"), + }, + { + "Undefined/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Undefined, err: errors.New("1"), errAfter: llvrwReadUndefined}, + errors.New("1"), + }, + { + "Undefined/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Undefined, err: errors.New("2"), errAfter: llvrwWriteUndefined}, + &TestValueReaderWriter{bsontype: bsontype.Undefined}, + errors.New("2"), + }, + { + "ObjectID/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.ObjectID, err: errors.New("1"), errAfter: llvrwReadObjectID}, + errors.New("1"), + }, + { + "ObjectID/dst/error", + &TestValueReaderWriter{bsontype: bsontype.ObjectID, err: errors.New("2"), errAfter: llvrwWriteObjectID}, + &TestValueReaderWriter{bsontype: bsontype.ObjectID, readval: primitive.ObjectID{0x01, 0x02, 0x03}}, + errors.New("2"), + }, + { + "Boolean/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Boolean, err: errors.New("1"), errAfter: llvrwReadBoolean}, + errors.New("1"), + }, + { + "Boolean/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Boolean, err: errors.New("2"), errAfter: llvrwWriteBoolean}, + &TestValueReaderWriter{bsontype: bsontype.Boolean, readval: bool(true)}, + errors.New("2"), + }, + { + "DateTime/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.DateTime, err: errors.New("1"), errAfter: llvrwReadDateTime}, + errors.New("1"), + }, + { + "DateTime/dst/error", + &TestValueReaderWriter{bsontype: bsontype.DateTime, err: errors.New("2"), errAfter: llvrwWriteDateTime}, + &TestValueReaderWriter{bsontype: bsontype.DateTime, readval: int64(1234567890)}, + errors.New("2"), + }, + { + "Null/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Null, err: errors.New("1"), errAfter: llvrwReadNull}, + errors.New("1"), + }, + { + "Null/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Null, err: errors.New("2"), errAfter: llvrwWriteNull}, + &TestValueReaderWriter{bsontype: bsontype.Null}, + errors.New("2"), + }, + { + "Regex/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Regex, err: errors.New("1"), errAfter: llvrwReadRegex}, + errors.New("1"), + }, + { + "Regex/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Regex, err: errors.New("2"), errAfter: llvrwWriteRegex}, + &TestValueReaderWriter{ + bsontype: bsontype.Regex, + readval: bsoncore.Value{ + Type: bsontype.Regex, + Data: bsoncore.AppendRegex(nil, "hello", "world"), + }, + }, + errors.New("2"), + }, + { + "DBPointer/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.DBPointer, err: errors.New("1"), errAfter: llvrwReadDBPointer}, + errors.New("1"), + }, + { + "DBPointer/dst/error", + &TestValueReaderWriter{bsontype: bsontype.DBPointer, err: errors.New("2"), errAfter: llvrwWriteDBPointer}, + &TestValueReaderWriter{ + bsontype: bsontype.DBPointer, + readval: bsoncore.Value{ + Type: bsontype.DBPointer, + Data: bsoncore.AppendDBPointer(nil, "foo", primitive.ObjectID{0x01, 0x02, 0x03}), + }, + }, + errors.New("2"), + }, + { + "Javascript/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.JavaScript, err: errors.New("1"), errAfter: llvrwReadJavascript}, + errors.New("1"), + }, + { + "Javascript/dst/error", + &TestValueReaderWriter{bsontype: bsontype.JavaScript, err: errors.New("2"), errAfter: llvrwWriteJavascript}, + &TestValueReaderWriter{bsontype: bsontype.JavaScript, readval: "hello, world"}, + errors.New("2"), + }, + { + "Symbol/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Symbol, err: errors.New("1"), errAfter: llvrwReadSymbol}, + errors.New("1"), + }, + { + "Symbol/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Symbol, err: errors.New("2"), errAfter: llvrwWriteSymbol}, + &TestValueReaderWriter{ + bsontype: bsontype.Symbol, + readval: bsoncore.Value{ + Type: bsontype.Symbol, + Data: bsoncore.AppendSymbol(nil, "hello, world"), + }, + }, + errors.New("2"), + }, + { + "CodeWithScope/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.CodeWithScope, err: errors.New("1"), errAfter: llvrwReadCodeWithScope}, + errors.New("1"), + }, + { + "CodeWithScope/dst/error", + &TestValueReaderWriter{bsontype: bsontype.CodeWithScope, err: errors.New("2"), errAfter: llvrwWriteCodeWithScope}, + &TestValueReaderWriter{bsontype: bsontype.CodeWithScope}, + errors.New("2"), + }, + { + "CodeWithScope/dst/copyDocumentCore error", + &TestValueReaderWriter{err: errors.New("3"), errAfter: llvrwWriteDocumentElement}, + &TestValueReaderWriter{bsontype: bsontype.CodeWithScope}, + errors.New("3"), + }, + { + "Int32/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Int32, err: errors.New("1"), errAfter: llvrwReadInt32}, + errors.New("1"), + }, + { + "Int32/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Int32, err: errors.New("2"), errAfter: llvrwWriteInt32}, + &TestValueReaderWriter{bsontype: bsontype.Int32, readval: int32(12345)}, + errors.New("2"), + }, + { + "Timestamp/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Timestamp, err: errors.New("1"), errAfter: llvrwReadTimestamp}, + errors.New("1"), + }, + { + "Timestamp/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Timestamp, err: errors.New("2"), errAfter: llvrwWriteTimestamp}, + &TestValueReaderWriter{ + bsontype: bsontype.Timestamp, + readval: bsoncore.Value{ + Type: bsontype.Timestamp, + Data: bsoncore.AppendTimestamp(nil, 12345, 67890), + }, + }, + errors.New("2"), + }, + { + "Int64/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Int64, err: errors.New("1"), errAfter: llvrwReadInt64}, + errors.New("1"), + }, + { + "Int64/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Int64, err: errors.New("2"), errAfter: llvrwWriteInt64}, + &TestValueReaderWriter{bsontype: bsontype.Int64, readval: int64(1234567890)}, + errors.New("2"), + }, + { + "Decimal128/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.Decimal128, err: errors.New("1"), errAfter: llvrwReadDecimal128}, + errors.New("1"), + }, + { + "Decimal128/dst/error", + &TestValueReaderWriter{bsontype: bsontype.Decimal128, err: errors.New("2"), errAfter: llvrwWriteDecimal128}, + &TestValueReaderWriter{bsontype: bsontype.Decimal128, readval: primitive.NewDecimal128(12345, 67890)}, + errors.New("2"), + }, + { + "MinKey/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.MinKey, err: errors.New("1"), errAfter: llvrwReadMinKey}, + errors.New("1"), + }, + { + "MinKey/dst/error", + &TestValueReaderWriter{bsontype: bsontype.MinKey, err: errors.New("2"), errAfter: llvrwWriteMinKey}, + &TestValueReaderWriter{bsontype: bsontype.MinKey}, + errors.New("2"), + }, + { + "MaxKey/src/error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{bsontype: bsontype.MaxKey, err: errors.New("1"), errAfter: llvrwReadMaxKey}, + errors.New("1"), + }, + { + "MaxKey/dst/error", + &TestValueReaderWriter{bsontype: bsontype.MaxKey, err: errors.New("2"), errAfter: llvrwWriteMaxKey}, + &TestValueReaderWriter{bsontype: bsontype.MaxKey}, + errors.New("2"), + }, + { + "Unknown BSON type error", + &TestValueReaderWriter{}, + &TestValueReaderWriter{}, + fmt.Errorf("Cannot copy unknown BSON type %s", bsontype.Type(0)), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.dst.t, tc.src.t = t, t + err := Copier{}.CopyValue(tc.dst, tc.src) + if !compareErrors(err, tc.err) { + t.Errorf("Did not receive expected error. got %v; want %v", err, tc.err) + } + }) + } + }) + t.Run("CopyValueFromBytes", func(t *testing.T) { + t.Run("BytesWriter", func(t *testing.T) { + vw := newValueWriterFromSlice(make([]byte, 0)) + _, err := vw.WriteDocument() + noerr(t, err) + _, err = vw.WriteDocumentElement("foo") + noerr(t, err) + err = Copier{}.CopyValueFromBytes(vw, bsontype.String, bsoncore.AppendString(nil, "bar")) + noerr(t, err) + err = vw.WriteDocumentEnd() + noerr(t, err) + var idx int32 + want, err := bsoncore.AppendDocumentEnd( + bsoncore.AppendStringElement( + bsoncore.AppendDocumentStartInline(nil, &idx), + "foo", "bar", + ), + idx, + ) + noerr(t, err) + got := vw.buf + if !bytes.Equal(got, want) { + t.Errorf("Bytes are not equal. got %v; want %v", got, want) + } + }) + t.Run("Non BytesWriter", func(t *testing.T) { + llvrw := &TestValueReaderWriter{t: t} + err := Copier{}.CopyValueFromBytes(llvrw, bsontype.String, bsoncore.AppendString(nil, "bar")) + noerr(t, err) + got, want := llvrw.invoked, llvrwWriteString + if got != want { + t.Errorf("Incorrect method invoked on llvrw. got %v; want %v", got, want) + } + }) + }) + t.Run("CopyValueToBytes", func(t *testing.T) { + t.Run("BytesReader", func(t *testing.T) { + var idx int32 + b, err := bsoncore.AppendDocumentEnd( + bsoncore.AppendStringElement( + bsoncore.AppendDocumentStartInline(nil, &idx), + "hello", "world", + ), + idx, + ) + noerr(t, err) + vr := newValueReader(b) + _, err = vr.ReadDocument() + noerr(t, err) + _, _, err = vr.ReadElement() + noerr(t, err) + btype, got, err := Copier{}.CopyValueToBytes(vr) + noerr(t, err) + want := bsoncore.AppendString(nil, "world") + if btype != bsontype.String { + t.Errorf("Incorrect type returned. got %v; want %v", btype, bsontype.String) + } + if !bytes.Equal(got, want) { + t.Errorf("Bytes do not match. got %v; want %v", got, want) + } + }) + t.Run("Non BytesReader", func(t *testing.T) { + llvrw := &TestValueReaderWriter{t: t, bsontype: bsontype.String, readval: "Hello, world!"} + btype, got, err := Copier{}.CopyValueToBytes(llvrw) + noerr(t, err) + want := bsoncore.AppendString(nil, "Hello, world!") + if btype != bsontype.String { + t.Errorf("Incorrect type returned. got %v; want %v", btype, bsontype.String) + } + if !bytes.Equal(got, want) { + t.Errorf("Bytes do not match. got %v; want %v", got, want) + } + }) + }) + t.Run("AppendValueBytes", func(t *testing.T) { + t.Run("BytesReader", func(t *testing.T) { + var idx int32 + b, err := bsoncore.AppendDocumentEnd( + bsoncore.AppendStringElement( + bsoncore.AppendDocumentStartInline(nil, &idx), + "hello", "world", + ), + idx, + ) + noerr(t, err) + vr := newValueReader(b) + _, err = vr.ReadDocument() + noerr(t, err) + _, _, err = vr.ReadElement() + noerr(t, err) + btype, got, err := Copier{}.AppendValueBytes(nil, vr) + noerr(t, err) + want := bsoncore.AppendString(nil, "world") + if btype != bsontype.String { + t.Errorf("Incorrect type returned. got %v; want %v", btype, bsontype.String) + } + if !bytes.Equal(got, want) { + t.Errorf("Bytes do not match. got %v; want %v", got, want) + } + }) + t.Run("Non BytesReader", func(t *testing.T) { + llvrw := &TestValueReaderWriter{t: t, bsontype: bsontype.String, readval: "Hello, world!"} + btype, got, err := Copier{}.AppendValueBytes(nil, llvrw) + noerr(t, err) + want := bsoncore.AppendString(nil, "Hello, world!") + if btype != bsontype.String { + t.Errorf("Incorrect type returned. got %v; want %v", btype, bsontype.String) + } + if !bytes.Equal(got, want) { + t.Errorf("Bytes do not match. got %v; want %v", got, want) + } + }) + t.Run("CopyValue error", func(t *testing.T) { + want := errors.New("CopyValue error") + llvrw := &TestValueReaderWriter{t: t, bsontype: bsontype.String, err: want, errAfter: llvrwReadString} + _, _, got := Copier{}.AppendValueBytes(make([]byte, 0), llvrw) + if !compareErrors(got, want) { + t.Errorf("Errors do not match. got %v; want %v", got, want) + } + }) + }) +} diff --git a/mongo/bson/bsonrw/doc.go b/mongo/bson/bsonrw/doc.go new file mode 100644 index 0000000..750b0d2 --- /dev/null +++ b/mongo/bson/bsonrw/doc.go @@ -0,0 +1,9 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +// Package bsonrw contains abstractions for reading and writing +// BSON and BSON like types from sources. +package bsonrw // import "go.mongodb.org/mongo-driver/bson/bsonrw" diff --git a/mongo/bson/bsonrw/extjson_parser.go b/mongo/bson/bsonrw/extjson_parser.go new file mode 100644 index 0000000..54c76bf --- /dev/null +++ b/mongo/bson/bsonrw/extjson_parser.go @@ -0,0 +1,806 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +const maxNestingDepth = 200 + +// ErrInvalidJSON indicates the JSON input is invalid +var ErrInvalidJSON = errors.New("invalid JSON input") + +type jsonParseState byte + +const ( + jpsStartState jsonParseState = iota + jpsSawBeginObject + jpsSawEndObject + jpsSawBeginArray + jpsSawEndArray + jpsSawColon + jpsSawComma + jpsSawKey + jpsSawValue + jpsDoneState + jpsInvalidState +) + +type jsonParseMode byte + +const ( + jpmInvalidMode jsonParseMode = iota + jpmObjectMode + jpmArrayMode +) + +type extJSONValue struct { + t bsontype.Type + v interface{} +} + +type extJSONObject struct { + keys []string + values []*extJSONValue +} + +type extJSONParser struct { + js *jsonScanner + s jsonParseState + m []jsonParseMode + k string + v *extJSONValue + + err error + canonical bool + depth int + maxDepth int + + emptyObject bool + relaxedUUID bool +} + +// newExtJSONParser returns a new extended JSON parser, ready to to begin +// parsing from the first character of the argued json input. It will not +// perform any read-ahead and will therefore not report any errors about +// malformed JSON at this point. +func newExtJSONParser(r io.Reader, canonical bool) *extJSONParser { + return &extJSONParser{ + js: &jsonScanner{r: r}, + s: jpsStartState, + m: []jsonParseMode{}, + canonical: canonical, + maxDepth: maxNestingDepth, + } +} + +// peekType examines the next value and returns its BSON Type +func (ejp *extJSONParser) peekType() (bsontype.Type, error) { + var t bsontype.Type + var err error + initialState := ejp.s + + ejp.advanceState() + switch ejp.s { + case jpsSawValue: + t = ejp.v.t + case jpsSawBeginArray: + t = bsontype.Array + case jpsInvalidState: + err = ejp.err + case jpsSawComma: + // in array mode, seeing a comma means we need to progress again to actually observe a type + if ejp.peekMode() == jpmArrayMode { + return ejp.peekType() + } + case jpsSawEndArray: + // this would only be a valid state if we were in array mode, so return end-of-array error + err = ErrEOA + case jpsSawBeginObject: + // peek key to determine type + ejp.advanceState() + switch ejp.s { + case jpsSawEndObject: // empty embedded document + t = bsontype.EmbeddedDocument + ejp.emptyObject = true + case jpsInvalidState: + err = ejp.err + case jpsSawKey: + if initialState == jpsStartState { + return bsontype.EmbeddedDocument, nil + } + t = wrapperKeyBSONType(ejp.k) + + // if $uuid is encountered, parse as binary subtype 4 + if ejp.k == "$uuid" { + ejp.relaxedUUID = true + t = bsontype.Binary + } + + switch t { + case bsontype.JavaScript: + // just saw $code, need to check for $scope at same level + _, err = ejp.readValue(bsontype.JavaScript) + if err != nil { + break + } + + switch ejp.s { + case jpsSawEndObject: // type is TypeJavaScript + case jpsSawComma: + ejp.advanceState() + + if ejp.s == jpsSawKey && ejp.k == "$scope" { + t = bsontype.CodeWithScope + } else { + err = fmt.Errorf("invalid extended JSON: unexpected key %s in CodeWithScope object", ejp.k) + } + case jpsInvalidState: + err = ejp.err + default: + err = ErrInvalidJSON + } + case bsontype.CodeWithScope: + err = errors.New("invalid extended JSON: code with $scope must contain $code before $scope") + } + } + } + + return t, err +} + +// readKey parses the next key and its type and returns them +func (ejp *extJSONParser) readKey() (string, bsontype.Type, error) { + if ejp.emptyObject { + ejp.emptyObject = false + return "", 0, ErrEOD + } + + // advance to key (or return with error) + switch ejp.s { + case jpsStartState: + ejp.advanceState() + if ejp.s == jpsSawBeginObject { + ejp.advanceState() + } + case jpsSawBeginObject: + ejp.advanceState() + case jpsSawValue, jpsSawEndObject, jpsSawEndArray: + ejp.advanceState() + switch ejp.s { + case jpsSawBeginObject, jpsSawComma: + ejp.advanceState() + case jpsSawEndObject: + return "", 0, ErrEOD + case jpsDoneState: + return "", 0, io.EOF + case jpsInvalidState: + return "", 0, ejp.err + default: + return "", 0, ErrInvalidJSON + } + case jpsSawKey: // do nothing (key was peeked before) + default: + return "", 0, invalidRequestError("key") + } + + // read key + var key string + + switch ejp.s { + case jpsSawKey: + key = ejp.k + case jpsSawEndObject: + return "", 0, ErrEOD + case jpsInvalidState: + return "", 0, ejp.err + default: + return "", 0, invalidRequestError("key") + } + + // check for colon + ejp.advanceState() + if err := ensureColon(ejp.s, key); err != nil { + return "", 0, err + } + + // peek at the value to determine type + t, err := ejp.peekType() + if err != nil { + return "", 0, err + } + + return key, t, nil +} + +// readValue returns the value corresponding to the Type returned by peekType +func (ejp *extJSONParser) readValue(t bsontype.Type) (*extJSONValue, error) { + if ejp.s == jpsInvalidState { + return nil, ejp.err + } + + var v *extJSONValue + + switch t { + case bsontype.Null, bsontype.Boolean, bsontype.String: + if ejp.s != jpsSawValue { + return nil, invalidRequestError(t.String()) + } + v = ejp.v + case bsontype.Int32, bsontype.Int64, bsontype.Double: + // relaxed version allows these to be literal number values + if ejp.s == jpsSawValue { + v = ejp.v + break + } + fallthrough + case bsontype.Decimal128, bsontype.Symbol, bsontype.ObjectID, bsontype.MinKey, bsontype.MaxKey, bsontype.Undefined: + switch ejp.s { + case jpsSawKey: + // read colon + ejp.advanceState() + if err := ensureColon(ejp.s, ejp.k); err != nil { + return nil, err + } + + // read value + ejp.advanceState() + if ejp.s != jpsSawValue || !ejp.ensureExtValueType(t) { + return nil, invalidJSONErrorForType("value", t) + } + + v = ejp.v + + // read end object + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, invalidJSONErrorForType("} after value", t) + } + default: + return nil, invalidRequestError(t.String()) + } + case bsontype.Binary, bsontype.Regex, bsontype.Timestamp, bsontype.DBPointer: + if ejp.s != jpsSawKey { + return nil, invalidRequestError(t.String()) + } + // read colon + ejp.advanceState() + if err := ensureColon(ejp.s, ejp.k); err != nil { + return nil, err + } + + ejp.advanceState() + if t == bsontype.Binary && ejp.s == jpsSawValue { + // convert relaxed $uuid format + if ejp.relaxedUUID { + defer func() { ejp.relaxedUUID = false }() + uuid, err := ejp.v.parseSymbol() + if err != nil { + return nil, err + } + + // RFC 4122 defines the length of a UUID as 36 and the hyphens in a UUID as appearing + // in the 8th, 13th, 18th, and 23rd characters. + // + // See https://tools.ietf.org/html/rfc4122#section-3 + valid := len(uuid) == 36 && + string(uuid[8]) == "-" && + string(uuid[13]) == "-" && + string(uuid[18]) == "-" && + string(uuid[23]) == "-" + if !valid { + return nil, fmt.Errorf("$uuid value does not follow RFC 4122 format regarding length and hyphens") + } + + // remove hyphens + uuidNoHyphens := strings.Replace(uuid, "-", "", -1) + if len(uuidNoHyphens) != 32 { + return nil, fmt.Errorf("$uuid value does not follow RFC 4122 format regarding length and hyphens") + } + + // convert hex to bytes + bytes, err := hex.DecodeString(uuidNoHyphens) + if err != nil { + return nil, fmt.Errorf("$uuid value does not follow RFC 4122 format regarding hex bytes: %v", err) + } + + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, invalidJSONErrorForType("$uuid and value and then }", bsontype.Binary) + } + + base64 := &extJSONValue{ + t: bsontype.String, + v: base64.StdEncoding.EncodeToString(bytes), + } + subType := &extJSONValue{ + t: bsontype.String, + v: "04", + } + + v = &extJSONValue{ + t: bsontype.EmbeddedDocument, + v: &extJSONObject{ + keys: []string{"base64", "subType"}, + values: []*extJSONValue{base64, subType}, + }, + } + + break + } + + // convert legacy $binary format + base64 := ejp.v + + ejp.advanceState() + if ejp.s != jpsSawComma { + return nil, invalidJSONErrorForType(",", bsontype.Binary) + } + + ejp.advanceState() + key, t, err := ejp.readKey() + if err != nil { + return nil, err + } + if key != "$type" { + return nil, invalidJSONErrorForType("$type", bsontype.Binary) + } + + subType, err := ejp.readValue(t) + if err != nil { + return nil, err + } + + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, invalidJSONErrorForType("2 key-value pairs and then }", bsontype.Binary) + } + + v = &extJSONValue{ + t: bsontype.EmbeddedDocument, + v: &extJSONObject{ + keys: []string{"base64", "subType"}, + values: []*extJSONValue{base64, subType}, + }, + } + break + } + + // read KV pairs + if ejp.s != jpsSawBeginObject { + return nil, invalidJSONErrorForType("{", t) + } + + keys, vals, err := ejp.readObject(2, true) + if err != nil { + return nil, err + } + + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, invalidJSONErrorForType("2 key-value pairs and then }", t) + } + + v = &extJSONValue{t: bsontype.EmbeddedDocument, v: &extJSONObject{keys: keys, values: vals}} + + case bsontype.DateTime: + switch ejp.s { + case jpsSawValue: + v = ejp.v + case jpsSawKey: + // read colon + ejp.advanceState() + if err := ensureColon(ejp.s, ejp.k); err != nil { + return nil, err + } + + ejp.advanceState() + switch ejp.s { + case jpsSawBeginObject: + keys, vals, err := ejp.readObject(1, true) + if err != nil { + return nil, err + } + v = &extJSONValue{t: bsontype.EmbeddedDocument, v: &extJSONObject{keys: keys, values: vals}} + case jpsSawValue: + if ejp.canonical { + return nil, invalidJSONError("{") + } + v = ejp.v + default: + if ejp.canonical { + return nil, invalidJSONErrorForType("object", t) + } + return nil, invalidJSONErrorForType("ISO-8601 Internet Date/Time Format as described in RFC-3339", t) + } + + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, invalidJSONErrorForType("value and then }", t) + } + default: + return nil, invalidRequestError(t.String()) + } + case bsontype.JavaScript: + switch ejp.s { + case jpsSawKey: + // read colon + ejp.advanceState() + if err := ensureColon(ejp.s, ejp.k); err != nil { + return nil, err + } + + // read value + ejp.advanceState() + if ejp.s != jpsSawValue { + return nil, invalidJSONErrorForType("value", t) + } + v = ejp.v + + // read end object or comma and just return + ejp.advanceState() + case jpsSawEndObject: + v = ejp.v + default: + return nil, invalidRequestError(t.String()) + } + case bsontype.CodeWithScope: + if ejp.s == jpsSawKey && ejp.k == "$scope" { + v = ejp.v // this is the $code string from earlier + + // read colon + ejp.advanceState() + if err := ensureColon(ejp.s, ejp.k); err != nil { + return nil, err + } + + // read { + ejp.advanceState() + if ejp.s != jpsSawBeginObject { + return nil, invalidJSONError("$scope to be embedded document") + } + } else { + return nil, invalidRequestError(t.String()) + } + case bsontype.EmbeddedDocument, bsontype.Array: + return nil, invalidRequestError(t.String()) + } + + return v, nil +} + +// readObject is a utility method for reading full objects of known (or expected) size +// it is useful for extended JSON types such as binary, datetime, regex, and timestamp +func (ejp *extJSONParser) readObject(numKeys int, started bool) ([]string, []*extJSONValue, error) { + keys := make([]string, numKeys) + vals := make([]*extJSONValue, numKeys) + + if !started { + ejp.advanceState() + if ejp.s != jpsSawBeginObject { + return nil, nil, invalidJSONError("{") + } + } + + for i := 0; i < numKeys; i++ { + key, t, err := ejp.readKey() + if err != nil { + return nil, nil, err + } + + switch ejp.s { + case jpsSawKey: + v, err := ejp.readValue(t) + if err != nil { + return nil, nil, err + } + + keys[i] = key + vals[i] = v + case jpsSawValue: + keys[i] = key + vals[i] = ejp.v + default: + return nil, nil, invalidJSONError("value") + } + } + + ejp.advanceState() + if ejp.s != jpsSawEndObject { + return nil, nil, invalidJSONError("}") + } + + return keys, vals, nil +} + +// advanceState reads the next JSON token from the scanner and transitions +// from the current state based on that token's type +func (ejp *extJSONParser) advanceState() { + if ejp.s == jpsDoneState || ejp.s == jpsInvalidState { + return + } + + jt, err := ejp.js.nextToken() + + if err != nil { + ejp.err = err + ejp.s = jpsInvalidState + return + } + + valid := ejp.validateToken(jt.t) + if !valid { + ejp.err = unexpectedTokenError(jt) + ejp.s = jpsInvalidState + return + } + + switch jt.t { + case jttBeginObject: + ejp.s = jpsSawBeginObject + ejp.pushMode(jpmObjectMode) + ejp.depth++ + + if ejp.depth > ejp.maxDepth { + ejp.err = nestingDepthError(jt.p, ejp.depth) + ejp.s = jpsInvalidState + } + case jttEndObject: + ejp.s = jpsSawEndObject + ejp.depth-- + + if ejp.popMode() != jpmObjectMode { + ejp.err = unexpectedTokenError(jt) + ejp.s = jpsInvalidState + } + case jttBeginArray: + ejp.s = jpsSawBeginArray + ejp.pushMode(jpmArrayMode) + case jttEndArray: + ejp.s = jpsSawEndArray + + if ejp.popMode() != jpmArrayMode { + ejp.err = unexpectedTokenError(jt) + ejp.s = jpsInvalidState + } + case jttColon: + ejp.s = jpsSawColon + case jttComma: + ejp.s = jpsSawComma + case jttEOF: + ejp.s = jpsDoneState + if len(ejp.m) != 0 { + ejp.err = unexpectedTokenError(jt) + ejp.s = jpsInvalidState + } + case jttString: + switch ejp.s { + case jpsSawComma: + if ejp.peekMode() == jpmArrayMode { + ejp.s = jpsSawValue + ejp.v = extendJSONToken(jt) + return + } + fallthrough + case jpsSawBeginObject: + ejp.s = jpsSawKey + ejp.k = jt.v.(string) + return + } + fallthrough + default: + ejp.s = jpsSawValue + ejp.v = extendJSONToken(jt) + } +} + +var jpsValidTransitionTokens = map[jsonParseState]map[jsonTokenType]bool{ + jpsStartState: { + jttBeginObject: true, + jttBeginArray: true, + jttInt32: true, + jttInt64: true, + jttDouble: true, + jttString: true, + jttBool: true, + jttNull: true, + jttEOF: true, + }, + jpsSawBeginObject: { + jttEndObject: true, + jttString: true, + }, + jpsSawEndObject: { + jttEndObject: true, + jttEndArray: true, + jttComma: true, + jttEOF: true, + }, + jpsSawBeginArray: { + jttBeginObject: true, + jttBeginArray: true, + jttEndArray: true, + jttInt32: true, + jttInt64: true, + jttDouble: true, + jttString: true, + jttBool: true, + jttNull: true, + }, + jpsSawEndArray: { + jttEndObject: true, + jttEndArray: true, + jttComma: true, + jttEOF: true, + }, + jpsSawColon: { + jttBeginObject: true, + jttBeginArray: true, + jttInt32: true, + jttInt64: true, + jttDouble: true, + jttString: true, + jttBool: true, + jttNull: true, + }, + jpsSawComma: { + jttBeginObject: true, + jttBeginArray: true, + jttInt32: true, + jttInt64: true, + jttDouble: true, + jttString: true, + jttBool: true, + jttNull: true, + }, + jpsSawKey: { + jttColon: true, + }, + jpsSawValue: { + jttEndObject: true, + jttEndArray: true, + jttComma: true, + jttEOF: true, + }, + jpsDoneState: {}, + jpsInvalidState: {}, +} + +func (ejp *extJSONParser) validateToken(jtt jsonTokenType) bool { + switch ejp.s { + case jpsSawEndObject: + // if we are at depth zero and the next token is a '{', + // we can consider it valid only if we are not in array mode. + if jtt == jttBeginObject && ejp.depth == 0 { + return ejp.peekMode() != jpmArrayMode + } + case jpsSawComma: + switch ejp.peekMode() { + // the only valid next token after a comma inside a document is a string (a key) + case jpmObjectMode: + return jtt == jttString + case jpmInvalidMode: + return false + } + } + + _, ok := jpsValidTransitionTokens[ejp.s][jtt] + return ok +} + +// ensureExtValueType returns true if the current value has the expected +// value type for single-key extended JSON types. For example, +// {"$numberInt": v} v must be TypeString +func (ejp *extJSONParser) ensureExtValueType(t bsontype.Type) bool { + switch t { + case bsontype.MinKey, bsontype.MaxKey: + return ejp.v.t == bsontype.Int32 + case bsontype.Undefined: + return ejp.v.t == bsontype.Boolean + case bsontype.Int32, bsontype.Int64, bsontype.Double, bsontype.Decimal128, bsontype.Symbol, bsontype.ObjectID: + return ejp.v.t == bsontype.String + default: + return false + } +} + +func (ejp *extJSONParser) pushMode(m jsonParseMode) { + ejp.m = append(ejp.m, m) +} + +func (ejp *extJSONParser) popMode() jsonParseMode { + l := len(ejp.m) + if l == 0 { + return jpmInvalidMode + } + + m := ejp.m[l-1] + ejp.m = ejp.m[:l-1] + + return m +} + +func (ejp *extJSONParser) peekMode() jsonParseMode { + l := len(ejp.m) + if l == 0 { + return jpmInvalidMode + } + + return ejp.m[l-1] +} + +func extendJSONToken(jt *jsonToken) *extJSONValue { + var t bsontype.Type + + switch jt.t { + case jttInt32: + t = bsontype.Int32 + case jttInt64: + t = bsontype.Int64 + case jttDouble: + t = bsontype.Double + case jttString: + t = bsontype.String + case jttBool: + t = bsontype.Boolean + case jttNull: + t = bsontype.Null + default: + return nil + } + + return &extJSONValue{t: t, v: jt.v} +} + +func ensureColon(s jsonParseState, key string) error { + if s != jpsSawColon { + return fmt.Errorf("invalid JSON input: missing colon after key \"%s\"", key) + } + + return nil +} + +func invalidRequestError(s string) error { + return fmt.Errorf("invalid request to read %s", s) +} + +func invalidJSONError(expected string) error { + return fmt.Errorf("invalid JSON input; expected %s", expected) +} + +func invalidJSONErrorForType(expected string, t bsontype.Type) error { + return fmt.Errorf("invalid JSON input; expected %s for %s", expected, t) +} + +func unexpectedTokenError(jt *jsonToken) error { + switch jt.t { + case jttInt32, jttInt64, jttDouble: + return fmt.Errorf("invalid JSON input; unexpected number (%v) at position %d", jt.v, jt.p) + case jttString: + return fmt.Errorf("invalid JSON input; unexpected string (\"%v\") at position %d", jt.v, jt.p) + case jttBool: + return fmt.Errorf("invalid JSON input; unexpected boolean literal (%v) at position %d", jt.v, jt.p) + case jttNull: + return fmt.Errorf("invalid JSON input; unexpected null literal at position %d", jt.p) + case jttEOF: + return fmt.Errorf("invalid JSON input; unexpected end of input at position %d", jt.p) + default: + return fmt.Errorf("invalid JSON input; unexpected %c at position %d", jt.v.(byte), jt.p) + } +} + +func nestingDepthError(p, depth int) error { + return fmt.Errorf("invalid JSON input; nesting too deep (%d levels) at position %d", depth, p) +} diff --git a/mongo/bson/bsonrw/extjson_parser_test.go b/mongo/bson/bsonrw/extjson_parser_test.go new file mode 100644 index 0000000..6808b14 --- /dev/null +++ b/mongo/bson/bsonrw/extjson_parser_test.go @@ -0,0 +1,788 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +var ( + keyDiff = specificDiff("key") + typDiff = specificDiff("type") + valDiff = specificDiff("value") + + expectErrEOF = expectSpecificError(io.EOF) + expectErrEOD = expectSpecificError(ErrEOD) + expectErrEOA = expectSpecificError(ErrEOA) +) + +type expectedErrorFunc func(t *testing.T, err error, desc string) + +type peekTypeTestCase struct { + desc string + input string + typs []bsontype.Type + errFs []expectedErrorFunc +} + +type readKeyValueTestCase struct { + desc string + input string + keys []string + typs []bsontype.Type + vals []*extJSONValue + + keyEFs []expectedErrorFunc + valEFs []expectedErrorFunc +} + +func expectSpecificError(expected error) expectedErrorFunc { + return func(t *testing.T, err error, desc string) { + if err != expected { + t.Helper() + t.Errorf("%s: Expected %v but got: %v", desc, expected, err) + t.FailNow() + } + } +} + +func specificDiff(name string) func(t *testing.T, expected, actual interface{}, desc string) { + return func(t *testing.T, expected, actual interface{}, desc string) { + if diff := cmp.Diff(expected, actual); diff != "" { + t.Helper() + t.Errorf("%s: Incorrect JSON %s (-want, +got): %s\n", desc, name, diff) + t.FailNow() + } + } +} + +func expectErrorNOOP(_ *testing.T, _ error, _ string) { +} + +func readKeyDiff(t *testing.T, eKey, aKey string, eTyp, aTyp bsontype.Type, err error, errF expectedErrorFunc, desc string) { + keyDiff(t, eKey, aKey, desc) + typDiff(t, eTyp, aTyp, desc) + errF(t, err, desc) +} + +func readValueDiff(t *testing.T, eVal, aVal *extJSONValue, err error, errF expectedErrorFunc, desc string) { + if aVal != nil { + typDiff(t, eVal.t, aVal.t, desc) + valDiff(t, eVal.v, aVal.v, desc) + } else { + valDiff(t, eVal, aVal, desc) + } + + errF(t, err, desc) +} + +func TestExtJSONParserPeekType(t *testing.T) { + makeValidPeekTypeTestCase := func(input string, typ bsontype.Type, desc string) peekTypeTestCase { + return peekTypeTestCase{ + desc: desc, input: input, + typs: []bsontype.Type{typ}, + errFs: []expectedErrorFunc{expectNoError}, + } + } + makeInvalidTestCase := func(desc, input string, lastEF expectedErrorFunc) peekTypeTestCase { + return peekTypeTestCase{ + desc: desc, input: input, + typs: []bsontype.Type{bsontype.Type(0)}, + errFs: []expectedErrorFunc{lastEF}, + } + } + + makeInvalidPeekTypeTestCase := func(desc, input string, lastEF expectedErrorFunc) peekTypeTestCase { + return peekTypeTestCase{ + desc: desc, input: input, + typs: []bsontype.Type{bsontype.Array, bsontype.String, bsontype.Type(0)}, + errFs: []expectedErrorFunc{expectNoError, expectNoError, lastEF}, + } + } + + cases := []peekTypeTestCase{ + makeValidPeekTypeTestCase(`null`, bsontype.Null, "Null"), + makeValidPeekTypeTestCase(`"string"`, bsontype.String, "String"), + makeValidPeekTypeTestCase(`true`, bsontype.Boolean, "Boolean--true"), + makeValidPeekTypeTestCase(`false`, bsontype.Boolean, "Boolean--false"), + makeValidPeekTypeTestCase(`{"$minKey": 1}`, bsontype.MinKey, "MinKey"), + makeValidPeekTypeTestCase(`{"$maxKey": 1}`, bsontype.MaxKey, "MaxKey"), + makeValidPeekTypeTestCase(`{"$numberInt": "42"}`, bsontype.Int32, "Int32"), + makeValidPeekTypeTestCase(`{"$numberLong": "42"}`, bsontype.Int64, "Int64"), + makeValidPeekTypeTestCase(`{"$symbol": "symbol"}`, bsontype.Symbol, "Symbol"), + makeValidPeekTypeTestCase(`{"$numberDouble": "42.42"}`, bsontype.Double, "Double"), + makeValidPeekTypeTestCase(`{"$undefined": true}`, bsontype.Undefined, "Undefined"), + makeValidPeekTypeTestCase(`{"$numberDouble": "NaN"}`, bsontype.Double, "Double--NaN"), + makeValidPeekTypeTestCase(`{"$numberDecimal": "1234"}`, bsontype.Decimal128, "Decimal"), + makeValidPeekTypeTestCase(`{"foo": "bar"}`, bsontype.EmbeddedDocument, "Toplevel document"), + makeValidPeekTypeTestCase(`{"$date": {"$numberLong": "0"}}`, bsontype.DateTime, "Datetime"), + makeValidPeekTypeTestCase(`{"$code": "function() {}"}`, bsontype.JavaScript, "Code no scope"), + makeValidPeekTypeTestCase(`[{"$numberInt": "1"},{"$numberInt": "2"}]`, bsontype.Array, "Array"), + makeValidPeekTypeTestCase(`{"$timestamp": {"t": 42, "i": 1}}`, bsontype.Timestamp, "Timestamp"), + makeValidPeekTypeTestCase(`{"$oid": "57e193d7a9cc81b4027498b5"}`, bsontype.ObjectID, "Object ID"), + makeValidPeekTypeTestCase(`{"$binary": {"base64": "AQIDBAU=", "subType": "80"}}`, bsontype.Binary, "Binary"), + makeValidPeekTypeTestCase(`{"$code": "function() {}", "$scope": {}}`, bsontype.CodeWithScope, "Code With Scope"), + makeValidPeekTypeTestCase(`{"$binary": {"base64": "o0w498Or7cijeBSpkquNtg==", "subType": "03"}}`, bsontype.Binary, "Binary"), + makeValidPeekTypeTestCase(`{"$binary": "o0w498Or7cijeBSpkquNtg==", "$type": "03"}`, bsontype.Binary, "Binary"), + makeValidPeekTypeTestCase(`{"$regularExpression": {"pattern": "foo*", "options": "ix"}}`, bsontype.Regex, "Regular expression"), + makeValidPeekTypeTestCase(`{"$dbPointer": {"$ref": "db.collection", "$id": {"$oid": "57e193d7a9cc81b4027498b1"}}}`, bsontype.DBPointer, "DBPointer"), + makeValidPeekTypeTestCase(`{"$ref": "collection", "$id": {"$oid": "57fd71e96e32ab4225b723fb"}, "$db": "database"}`, bsontype.EmbeddedDocument, "DBRef"), + makeInvalidPeekTypeTestCase("invalid array--missing ]", `["a"`, expectError), + makeInvalidPeekTypeTestCase("invalid array--colon in array", `["a":`, expectError), + makeInvalidPeekTypeTestCase("invalid array--extra comma", `["a",,`, expectError), + makeInvalidPeekTypeTestCase("invalid array--trailing comma", `["a",]`, expectError), + makeInvalidPeekTypeTestCase("peekType after end of array", `["a"]`, expectErrEOA), + { + desc: "invalid array--leading comma", + input: `[,`, + typs: []bsontype.Type{bsontype.Array, bsontype.Type(0)}, + errFs: []expectedErrorFunc{expectNoError, expectError}, + }, + makeInvalidTestCase("lone $scope", `{"$scope": {}}`, expectError), + makeInvalidTestCase("empty code with unknown extra key", `{"$code":"", "0":""}`, expectError), + makeInvalidTestCase("non-empty code with unknown extra key", `{"$code":"foobar", "0":""}`, expectError), + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ejp := newExtJSONParser(strings.NewReader(tc.input), true) + // Manually set the parser's starting state to jpsSawColon so peekType will read ahead to find the extjson + // type of the value. If not set, the parser will be in jpsStartState and advance to jpsSawKey, which will + // cause it to return without peeking the extjson type. + ejp.s = jpsSawColon + + for i, eTyp := range tc.typs { + errF := tc.errFs[i] + + typ, err := ejp.peekType() + errF(t, err, tc.desc) + if err != nil { + // Don't inspect the type if there was an error + return + } + + typDiff(t, eTyp, typ, tc.desc) + } + }) + } +} + +func TestExtJSONParserReadKeyReadValue(t *testing.T) { + // several test cases will use the same keys, types, and values, and only differ on input structure + + keys := []string{"_id", "Symbol", "String", "Int32", "Int64", "Int", "MinKey"} + types := []bsontype.Type{bsontype.ObjectID, bsontype.Symbol, bsontype.String, bsontype.Int32, bsontype.Int64, bsontype.Int32, bsontype.MinKey} + values := []*extJSONValue{ + {t: bsontype.String, v: "57e193d7a9cc81b4027498b5"}, + {t: bsontype.String, v: "symbol"}, + {t: bsontype.String, v: "string"}, + {t: bsontype.String, v: "42"}, + {t: bsontype.String, v: "42"}, + {t: bsontype.Int32, v: int32(42)}, + {t: bsontype.Int32, v: int32(1)}, + } + + errFuncs := make([]expectedErrorFunc, 7) + for i := 0; i < 7; i++ { + errFuncs[i] = expectNoError + } + + firstKeyError := func(desc, input string) readKeyValueTestCase { + return readKeyValueTestCase{ + desc: desc, + input: input, + keys: []string{""}, + typs: []bsontype.Type{bsontype.Type(0)}, + vals: []*extJSONValue{nil}, + keyEFs: []expectedErrorFunc{expectError}, + valEFs: []expectedErrorFunc{expectErrorNOOP}, + } + } + + secondKeyError := func(desc, input, firstKey string, firstType bsontype.Type, firstValue *extJSONValue) readKeyValueTestCase { + return readKeyValueTestCase{ + desc: desc, + input: input, + keys: []string{firstKey, ""}, + typs: []bsontype.Type{firstType, bsontype.Type(0)}, + vals: []*extJSONValue{firstValue, nil}, + keyEFs: []expectedErrorFunc{expectNoError, expectError}, + valEFs: []expectedErrorFunc{expectNoError, expectErrorNOOP}, + } + } + + cases := []readKeyValueTestCase{ + { + desc: "normal spacing", + input: `{ + "_id": { "$oid": "57e193d7a9cc81b4027498b5" }, + "Symbol": { "$symbol": "symbol" }, + "String": "string", + "Int32": { "$numberInt": "42" }, + "Int64": { "$numberLong": "42" }, + "Int": 42, + "MinKey": { "$minKey": 1 } + }`, + keys: keys, typs: types, vals: values, + keyEFs: errFuncs, valEFs: errFuncs, + }, + { + desc: "new line before comma", + input: `{ "_id": { "$oid": "57e193d7a9cc81b4027498b5" } + , "Symbol": { "$symbol": "symbol" } + , "String": "string" + , "Int32": { "$numberInt": "42" } + , "Int64": { "$numberLong": "42" } + , "Int": 42 + , "MinKey": { "$minKey": 1 } + }`, + keys: keys, typs: types, vals: values, + keyEFs: errFuncs, valEFs: errFuncs, + }, + { + desc: "tabs around colons", + input: `{ + "_id": { "$oid" : "57e193d7a9cc81b4027498b5" }, + "Symbol": { "$symbol" : "symbol" }, + "String": "string", + "Int32": { "$numberInt" : "42" }, + "Int64": { "$numberLong": "42" }, + "Int": 42, + "MinKey": { "$minKey": 1 } + }`, + keys: keys, typs: types, vals: values, + keyEFs: errFuncs, valEFs: errFuncs, + }, + { + desc: "no whitespace", + input: `{"_id":{"$oid":"57e193d7a9cc81b4027498b5"},"Symbol":{"$symbol":"symbol"},"String":"string","Int32":{"$numberInt":"42"},"Int64":{"$numberLong":"42"},"Int":42,"MinKey":{"$minKey":1}}`, + keys: keys, typs: types, vals: values, + keyEFs: errFuncs, valEFs: errFuncs, + }, + { + desc: "mixed whitespace", + input: ` { + "_id" : { "$oid": "57e193d7a9cc81b4027498b5" }, + "Symbol" : { "$symbol": "symbol" } , + "String" : "string", + "Int32" : { "$numberInt": "42" } , + "Int64" : {"$numberLong" : "42"}, + "Int" : 42, + "MinKey" : { "$minKey": 1 } } `, + keys: keys, typs: types, vals: values, + keyEFs: errFuncs, valEFs: errFuncs, + }, + { + desc: "nested object", + input: `{"k1": 1, "k2": { "k3": { "k4": 4 } }, "k5": 5}`, + keys: []string{"k1", "k2", "k3", "k4", "", "", "k5", ""}, + typs: []bsontype.Type{bsontype.Int32, bsontype.EmbeddedDocument, bsontype.EmbeddedDocument, bsontype.Int32, bsontype.Type(0), bsontype.Type(0), bsontype.Int32, bsontype.Type(0)}, + vals: []*extJSONValue{ + {t: bsontype.Int32, v: int32(1)}, nil, nil, {t: bsontype.Int32, v: int32(4)}, nil, nil, {t: bsontype.Int32, v: int32(5)}, nil, + }, + keyEFs: []expectedErrorFunc{ + expectNoError, expectNoError, expectNoError, expectNoError, expectErrEOD, + expectErrEOD, expectNoError, expectErrEOD, + }, + valEFs: []expectedErrorFunc{ + expectNoError, expectError, expectError, expectNoError, expectErrorNOOP, + expectErrorNOOP, expectNoError, expectErrorNOOP, + }, + }, + { + desc: "invalid input: invalid values for extended type", + input: `{"a": {"$numberInt": "1", "x"`, + keys: []string{"a"}, + typs: []bsontype.Type{bsontype.Int32}, + vals: []*extJSONValue{nil}, + keyEFs: []expectedErrorFunc{expectNoError}, + valEFs: []expectedErrorFunc{expectError}, + }, + firstKeyError("invalid input: missing key--EOF", "{"), + firstKeyError("invalid input: missing key--colon first", "{:"), + firstKeyError("invalid input: missing value", `{"a":`), + firstKeyError("invalid input: missing colon", `{"a" 1`), + firstKeyError("invalid input: extra colon", `{"a"::`), + secondKeyError("invalid input: missing }", `{"a": 1`, "a", bsontype.Int32, &extJSONValue{t: bsontype.Int32, v: int32(1)}), + secondKeyError("invalid input: missing comma", `{"a": 1 "b"`, "a", bsontype.Int32, &extJSONValue{t: bsontype.Int32, v: int32(1)}), + secondKeyError("invalid input: extra comma", `{"a": 1,, "b"`, "a", bsontype.Int32, &extJSONValue{t: bsontype.Int32, v: int32(1)}), + secondKeyError("invalid input: trailing comma in object", `{"a": 1,}`, "a", bsontype.Int32, &extJSONValue{t: bsontype.Int32, v: int32(1)}), + { + desc: "invalid input: lone scope after a complete value", + input: `{"a": "", "b": {"$scope: ""}}`, + keys: []string{"a"}, + typs: []bsontype.Type{bsontype.String}, + vals: []*extJSONValue{{bsontype.String, ""}}, + keyEFs: []expectedErrorFunc{expectNoError, expectNoError}, + valEFs: []expectedErrorFunc{expectNoError, expectError}, + }, + { + desc: "invalid input: lone scope nested", + input: `{"a":{"b":{"$scope":{`, + keys: []string{}, + typs: []bsontype.Type{}, + vals: []*extJSONValue{nil}, + keyEFs: []expectedErrorFunc{expectNoError}, + valEFs: []expectedErrorFunc{expectError}, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ejp := newExtJSONParser(strings.NewReader(tc.input), true) + + for i, eKey := range tc.keys { + eTyp := tc.typs[i] + eVal := tc.vals[i] + + keyErrF := tc.keyEFs[i] + valErrF := tc.valEFs[i] + + k, typ, err := ejp.readKey() + readKeyDiff(t, eKey, k, eTyp, typ, err, keyErrF, tc.desc) + + v, err := ejp.readValue(typ) + readValueDiff(t, eVal, v, err, valErrF, tc.desc) + } + }) + } +} + +type ejpExpectationTest func(t *testing.T, p *extJSONParser, expectedKey string, expectedType bsontype.Type, expectedValue interface{}) + +type ejpTestCase struct { + f ejpExpectationTest + p *extJSONParser + k string + t bsontype.Type + v interface{} +} + +// expectSingleValue is used for simple JSON types (strings, numbers, literals) and for extended JSON types that +// have single key-value pairs (i.e. { "$minKey": 1 }, { "$numberLong": "42.42" }) +func expectSingleValue(t *testing.T, p *extJSONParser, expectedKey string, expectedType bsontype.Type, expectedValue interface{}) { + eVal := expectedValue.(*extJSONValue) + + k, typ, err := p.readKey() + readKeyDiff(t, expectedKey, k, expectedType, typ, err, expectNoError, expectedKey) + + v, err := p.readValue(typ) + readValueDiff(t, eVal, v, err, expectNoError, expectedKey) +} + +// expectMultipleValues is used for values that are subdocuments of known size and with known keys (such as extended +// JSON types { "$timestamp": {"t": 1, "i": 1} } and { "$regularExpression": {"pattern": "", options: ""} }) +func expectMultipleValues(t *testing.T, p *extJSONParser, expectedKey string, expectedType bsontype.Type, expectedValue interface{}) { + k, typ, err := p.readKey() + readKeyDiff(t, expectedKey, k, expectedType, typ, err, expectNoError, expectedKey) + + v, err := p.readValue(typ) + expectNoError(t, err, "") + typDiff(t, bsontype.EmbeddedDocument, v.t, expectedKey) + + actObj := v.v.(*extJSONObject) + expObj := expectedValue.(*extJSONObject) + + for i, actKey := range actObj.keys { + expKey := expObj.keys[i] + actVal := actObj.values[i] + expVal := expObj.values[i] + + keyDiff(t, expKey, actKey, expectedKey) + typDiff(t, expVal.t, actVal.t, expectedKey) + valDiff(t, expVal.v, actVal.v, expectedKey) + } +} + +type ejpKeyTypValTriple struct { + key string + typ bsontype.Type + val *extJSONValue +} + +type ejpSubDocumentTestValue struct { + code string // code is only used for TypeCodeWithScope (and is ignored for TypeEmbeddedDocument + ktvs []ejpKeyTypValTriple // list of (key, type, value) triples; this is "scope" for TypeCodeWithScope +} + +// expectSubDocument is used for embedded documents and code with scope types; it reads all the keys and values +// in the embedded document (or scope for codeWithScope) and compares them to the expectedValue's list of (key, type, +// value) triples +func expectSubDocument(t *testing.T, p *extJSONParser, expectedKey string, expectedType bsontype.Type, expectedValue interface{}) { + subdoc := expectedValue.(ejpSubDocumentTestValue) + + k, typ, err := p.readKey() + readKeyDiff(t, expectedKey, k, expectedType, typ, err, expectNoError, expectedKey) + + if expectedType == bsontype.CodeWithScope { + v, err := p.readValue(typ) + readValueDiff(t, &extJSONValue{t: bsontype.String, v: subdoc.code}, v, err, expectNoError, expectedKey) + } + + for _, ktv := range subdoc.ktvs { + eKey := ktv.key + eTyp := ktv.typ + eVal := ktv.val + + k, typ, err = p.readKey() + readKeyDiff(t, eKey, k, eTyp, typ, err, expectNoError, expectedKey) + + v, err := p.readValue(typ) + readValueDiff(t, eVal, v, err, expectNoError, expectedKey) + } + + if expectedType == bsontype.CodeWithScope { + // expect scope doc to close + k, typ, err = p.readKey() + readKeyDiff(t, "", k, bsontype.Type(0), typ, err, expectErrEOD, expectedKey) + } + + // expect subdoc to close + k, typ, err = p.readKey() + readKeyDiff(t, "", k, bsontype.Type(0), typ, err, expectErrEOD, expectedKey) +} + +// expectArray takes the expectedKey, ignores the expectedType, and uses the expectedValue +// as a slice of (type Type, value *extJSONValue) pairs +func expectArray(t *testing.T, p *extJSONParser, expectedKey string, _ bsontype.Type, expectedValue interface{}) { + ktvs := expectedValue.([]ejpKeyTypValTriple) + + k, typ, err := p.readKey() + readKeyDiff(t, expectedKey, k, bsontype.Array, typ, err, expectNoError, expectedKey) + + for _, ktv := range ktvs { + eTyp := ktv.typ + eVal := ktv.val + + typ, err = p.peekType() + typDiff(t, eTyp, typ, expectedKey) + expectNoError(t, err, expectedKey) + + v, err := p.readValue(typ) + readValueDiff(t, eVal, v, err, expectNoError, expectedKey) + } + + // expect array to end + typ, err = p.peekType() + typDiff(t, bsontype.Type(0), typ, expectedKey) + expectErrEOA(t, err, expectedKey) +} + +func TestExtJSONParserAllTypes(t *testing.T) { + in := ` { "_id" : { "$oid": "57e193d7a9cc81b4027498b5"} + , "Symbol" : { "$symbol": "symbol"} + , "String" : "string" + , "Int32" : { "$numberInt": "42"} + , "Int64" : { "$numberLong": "42"} + , "Double" : { "$numberDouble": "42.42"} + , "SpecialFloat" : { "$numberDouble": "NaN" } + , "Decimal" : { "$numberDecimal": "1234" } + , "Binary" : { "$binary": { "base64": "o0w498Or7cijeBSpkquNtg==", "subType": "03" } } + , "BinaryLegacy" : { "$binary": "o0w498Or7cijeBSpkquNtg==", "$type": "03" } + , "BinaryUserDefined" : { "$binary": { "base64": "AQIDBAU=", "subType": "80" } } + , "Code" : { "$code": "function() {}" } + , "CodeWithEmptyScope" : { "$code": "function() {}", "$scope": {} } + , "CodeWithScope" : { "$code": "function() {}", "$scope": { "x": 1 } } + , "EmptySubdocument" : {} + , "Subdocument" : { "foo": "bar", "baz": { "$numberInt": "42" } } + , "Array" : [{"$numberInt": "1"}, {"$numberLong": "2"}, {"$numberDouble": "3"}, 4, "string", 5.0] + , "Timestamp" : { "$timestamp": { "t": 42, "i": 1 } } + , "RegularExpression" : { "$regularExpression": { "pattern": "foo*", "options": "ix" } } + , "DatetimeEpoch" : { "$date": { "$numberLong": "0" } } + , "DatetimePositive" : { "$date": { "$numberLong": "9223372036854775807" } } + , "DatetimeNegative" : { "$date": { "$numberLong": "-9223372036854775808" } } + , "True" : true + , "False" : false + , "DBPointer" : { "$dbPointer": { "$ref": "db.collection", "$id": { "$oid": "57e193d7a9cc81b4027498b1" } } } + , "DBRef" : { "$ref": "collection", "$id": { "$oid": "57fd71e96e32ab4225b723fb" }, "$db": "database" } + , "DBRefNoDB" : { "$ref": "collection", "$id": { "$oid": "57fd71e96e32ab4225b723fb" } } + , "MinKey" : { "$minKey": 1 } + , "MaxKey" : { "$maxKey": 1 } + , "Null" : null + , "Undefined" : { "$undefined": true } + }` + + ejp := newExtJSONParser(strings.NewReader(in), true) + + cases := []ejpTestCase{ + { + f: expectSingleValue, p: ejp, + k: "_id", t: bsontype.ObjectID, v: &extJSONValue{t: bsontype.String, v: "57e193d7a9cc81b4027498b5"}, + }, + { + f: expectSingleValue, p: ejp, + k: "Symbol", t: bsontype.Symbol, v: &extJSONValue{t: bsontype.String, v: "symbol"}, + }, + { + f: expectSingleValue, p: ejp, + k: "String", t: bsontype.String, v: &extJSONValue{t: bsontype.String, v: "string"}, + }, + { + f: expectSingleValue, p: ejp, + k: "Int32", t: bsontype.Int32, v: &extJSONValue{t: bsontype.String, v: "42"}, + }, + { + f: expectSingleValue, p: ejp, + k: "Int64", t: bsontype.Int64, v: &extJSONValue{t: bsontype.String, v: "42"}, + }, + { + f: expectSingleValue, p: ejp, + k: "Double", t: bsontype.Double, v: &extJSONValue{t: bsontype.String, v: "42.42"}, + }, + { + f: expectSingleValue, p: ejp, + k: "SpecialFloat", t: bsontype.Double, v: &extJSONValue{t: bsontype.String, v: "NaN"}, + }, + { + f: expectSingleValue, p: ejp, + k: "Decimal", t: bsontype.Decimal128, v: &extJSONValue{t: bsontype.String, v: "1234"}, + }, + { + f: expectMultipleValues, p: ejp, + k: "Binary", t: bsontype.Binary, + v: &extJSONObject{ + keys: []string{"base64", "subType"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "o0w498Or7cijeBSpkquNtg=="}, + {t: bsontype.String, v: "03"}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "BinaryLegacy", t: bsontype.Binary, + v: &extJSONObject{ + keys: []string{"base64", "subType"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "o0w498Or7cijeBSpkquNtg=="}, + {t: bsontype.String, v: "03"}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "BinaryUserDefined", t: bsontype.Binary, + v: &extJSONObject{ + keys: []string{"base64", "subType"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "AQIDBAU="}, + {t: bsontype.String, v: "80"}, + }, + }, + }, + { + f: expectSingleValue, p: ejp, + k: "Code", t: bsontype.JavaScript, v: &extJSONValue{t: bsontype.String, v: "function() {}"}, + }, + { + f: expectSubDocument, p: ejp, + k: "CodeWithEmptyScope", t: bsontype.CodeWithScope, + v: ejpSubDocumentTestValue{ + code: "function() {}", + ktvs: []ejpKeyTypValTriple{}, + }, + }, + { + f: expectSubDocument, p: ejp, + k: "CodeWithScope", t: bsontype.CodeWithScope, + v: ejpSubDocumentTestValue{ + code: "function() {}", + ktvs: []ejpKeyTypValTriple{ + {"x", bsontype.Int32, &extJSONValue{t: bsontype.Int32, v: int32(1)}}, + }, + }, + }, + { + f: expectSubDocument, p: ejp, + k: "EmptySubdocument", t: bsontype.EmbeddedDocument, + v: ejpSubDocumentTestValue{ + ktvs: []ejpKeyTypValTriple{}, + }, + }, + { + f: expectSubDocument, p: ejp, + k: "Subdocument", t: bsontype.EmbeddedDocument, + v: ejpSubDocumentTestValue{ + ktvs: []ejpKeyTypValTriple{ + {"foo", bsontype.String, &extJSONValue{t: bsontype.String, v: "bar"}}, + {"baz", bsontype.Int32, &extJSONValue{t: bsontype.String, v: "42"}}, + }, + }, + }, + { + f: expectArray, p: ejp, + k: "Array", t: bsontype.Array, + v: []ejpKeyTypValTriple{ + {typ: bsontype.Int32, val: &extJSONValue{t: bsontype.String, v: "1"}}, + {typ: bsontype.Int64, val: &extJSONValue{t: bsontype.String, v: "2"}}, + {typ: bsontype.Double, val: &extJSONValue{t: bsontype.String, v: "3"}}, + {typ: bsontype.Int32, val: &extJSONValue{t: bsontype.Int32, v: int32(4)}}, + {typ: bsontype.String, val: &extJSONValue{t: bsontype.String, v: "string"}}, + {typ: bsontype.Double, val: &extJSONValue{t: bsontype.Double, v: 5.0}}, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "Timestamp", t: bsontype.Timestamp, + v: &extJSONObject{ + keys: []string{"t", "i"}, + values: []*extJSONValue{ + {t: bsontype.Int32, v: int32(42)}, + {t: bsontype.Int32, v: int32(1)}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "RegularExpression", t: bsontype.Regex, + v: &extJSONObject{ + keys: []string{"pattern", "options"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "foo*"}, + {t: bsontype.String, v: "ix"}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "DatetimeEpoch", t: bsontype.DateTime, + v: &extJSONObject{ + keys: []string{"$numberLong"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "0"}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "DatetimePositive", t: bsontype.DateTime, + v: &extJSONObject{ + keys: []string{"$numberLong"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "9223372036854775807"}, + }, + }, + }, + { + f: expectMultipleValues, p: ejp, + k: "DatetimeNegative", t: bsontype.DateTime, + v: &extJSONObject{ + keys: []string{"$numberLong"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "-9223372036854775808"}, + }, + }, + }, + { + f: expectSingleValue, p: ejp, + k: "True", t: bsontype.Boolean, v: &extJSONValue{t: bsontype.Boolean, v: true}, + }, + { + f: expectSingleValue, p: ejp, + k: "False", t: bsontype.Boolean, v: &extJSONValue{t: bsontype.Boolean, v: false}, + }, + { + f: expectMultipleValues, p: ejp, + k: "DBPointer", t: bsontype.DBPointer, + v: &extJSONObject{ + keys: []string{"$ref", "$id"}, + values: []*extJSONValue{ + {t: bsontype.String, v: "db.collection"}, + {t: bsontype.String, v: "57e193d7a9cc81b4027498b1"}, + }, + }, + }, + { + f: expectSubDocument, p: ejp, + k: "DBRef", t: bsontype.EmbeddedDocument, + v: ejpSubDocumentTestValue{ + ktvs: []ejpKeyTypValTriple{ + {"$ref", bsontype.String, &extJSONValue{t: bsontype.String, v: "collection"}}, + {"$id", bsontype.ObjectID, &extJSONValue{t: bsontype.String, v: "57fd71e96e32ab4225b723fb"}}, + {"$db", bsontype.String, &extJSONValue{t: bsontype.String, v: "database"}}, + }, + }, + }, + { + f: expectSubDocument, p: ejp, + k: "DBRefNoDB", t: bsontype.EmbeddedDocument, + v: ejpSubDocumentTestValue{ + ktvs: []ejpKeyTypValTriple{ + {"$ref", bsontype.String, &extJSONValue{t: bsontype.String, v: "collection"}}, + {"$id", bsontype.ObjectID, &extJSONValue{t: bsontype.String, v: "57fd71e96e32ab4225b723fb"}}, + }, + }, + }, + { + f: expectSingleValue, p: ejp, + k: "MinKey", t: bsontype.MinKey, v: &extJSONValue{t: bsontype.Int32, v: int32(1)}, + }, + { + f: expectSingleValue, p: ejp, + k: "MaxKey", t: bsontype.MaxKey, v: &extJSONValue{t: bsontype.Int32, v: int32(1)}, + }, + { + f: expectSingleValue, p: ejp, + k: "Null", t: bsontype.Null, v: &extJSONValue{t: bsontype.Null, v: nil}, + }, + { + f: expectSingleValue, p: ejp, + k: "Undefined", t: bsontype.Undefined, v: &extJSONValue{t: bsontype.Boolean, v: true}, + }, + } + + // run the test cases + for _, tc := range cases { + tc.f(t, tc.p, tc.k, tc.t, tc.v) + } + + // expect end of whole document: read final } + k, typ, err := ejp.readKey() + readKeyDiff(t, "", k, bsontype.Type(0), typ, err, expectErrEOD, "") + + // expect end of whole document: read EOF + k, typ, err = ejp.readKey() + readKeyDiff(t, "", k, bsontype.Type(0), typ, err, expectErrEOF, "") + if diff := cmp.Diff(jpsDoneState, ejp.s); diff != "" { + t.Errorf("expected parser to be in done state but instead is in %v\n", ejp.s) + t.FailNow() + } +} + +func TestExtJSONValue(t *testing.T) { + t.Run("Large Date", func(t *testing.T) { + val := &extJSONValue{ + t: bsontype.String, + v: "3001-01-01T00:00:00Z", + } + + intVal, err := val.parseDateTime() + if err != nil { + t.Fatalf("error parsing date time: %v", err) + } + + if intVal <= 0 { + t.Fatalf("expected value above 0, got %v", intVal) + } + }) + t.Run("fallback time format", func(t *testing.T) { + val := &extJSONValue{ + t: bsontype.String, + v: "2019-06-04T14:54:31.416+0000", + } + + _, err := val.parseDateTime() + if err != nil { + t.Fatalf("error parsing date time: %v", err) + } + }) +} diff --git a/mongo/bson/bsonrw/extjson_reader.go b/mongo/bson/bsonrw/extjson_reader.go new file mode 100644 index 0000000..35832d7 --- /dev/null +++ b/mongo/bson/bsonrw/extjson_reader.go @@ -0,0 +1,644 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "fmt" + "io" + "sync" + + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ExtJSONValueReaderPool is a pool for ValueReaders that read ExtJSON. +type ExtJSONValueReaderPool struct { + pool sync.Pool +} + +// NewExtJSONValueReaderPool instantiates a new ExtJSONValueReaderPool. +func NewExtJSONValueReaderPool() *ExtJSONValueReaderPool { + return &ExtJSONValueReaderPool{ + pool: sync.Pool{ + New: func() interface{} { + return new(extJSONValueReader) + }, + }, + } +} + +// Get retrieves a ValueReader from the pool and uses src as the underlying ExtJSON. +func (bvrp *ExtJSONValueReaderPool) Get(r io.Reader, canonical bool) (ValueReader, error) { + vr := bvrp.pool.Get().(*extJSONValueReader) + return vr.reset(r, canonical) +} + +// Put inserts a ValueReader into the pool. If the ValueReader is not a ExtJSON ValueReader nothing +// is inserted into the pool and ok will be false. +func (bvrp *ExtJSONValueReaderPool) Put(vr ValueReader) (ok bool) { + bvr, ok := vr.(*extJSONValueReader) + if !ok { + return false + } + + bvr, _ = bvr.reset(nil, false) + bvrp.pool.Put(bvr) + return true +} + +type ejvrState struct { + mode mode + vType bsontype.Type + depth int +} + +// extJSONValueReader is for reading extended JSON. +type extJSONValueReader struct { + p *extJSONParser + + stack []ejvrState + frame int +} + +// NewExtJSONValueReader creates a new ValueReader from a given io.Reader +// It will interpret the JSON of r as canonical or relaxed according to the +// given canonical flag +func NewExtJSONValueReader(r io.Reader, canonical bool) (ValueReader, error) { + return newExtJSONValueReader(r, canonical) +} + +func newExtJSONValueReader(r io.Reader, canonical bool) (*extJSONValueReader, error) { + ejvr := new(extJSONValueReader) + return ejvr.reset(r, canonical) +} + +func (ejvr *extJSONValueReader) reset(r io.Reader, canonical bool) (*extJSONValueReader, error) { + p := newExtJSONParser(r, canonical) + typ, err := p.peekType() + + if err != nil { + return nil, ErrInvalidJSON + } + + var m mode + switch typ { + case bsontype.EmbeddedDocument: + m = mTopLevel + case bsontype.Array: + m = mArray + default: + m = mValue + } + + stack := make([]ejvrState, 1, 5) + stack[0] = ejvrState{ + mode: m, + vType: typ, + } + return &extJSONValueReader{ + p: p, + stack: stack, + }, nil +} + +func (ejvr *extJSONValueReader) advanceFrame() { + if ejvr.frame+1 >= len(ejvr.stack) { // We need to grow the stack + length := len(ejvr.stack) + if length+1 >= cap(ejvr.stack) { + // double it + buf := make([]ejvrState, 2*cap(ejvr.stack)+1) + copy(buf, ejvr.stack) + ejvr.stack = buf + } + ejvr.stack = ejvr.stack[:length+1] + } + ejvr.frame++ + + // Clean the stack + ejvr.stack[ejvr.frame].mode = 0 + ejvr.stack[ejvr.frame].vType = 0 + ejvr.stack[ejvr.frame].depth = 0 +} + +func (ejvr *extJSONValueReader) pushDocument() { + ejvr.advanceFrame() + + ejvr.stack[ejvr.frame].mode = mDocument + ejvr.stack[ejvr.frame].depth = ejvr.p.depth +} + +func (ejvr *extJSONValueReader) pushCodeWithScope() { + ejvr.advanceFrame() + + ejvr.stack[ejvr.frame].mode = mCodeWithScope +} + +func (ejvr *extJSONValueReader) pushArray() { + ejvr.advanceFrame() + + ejvr.stack[ejvr.frame].mode = mArray +} + +func (ejvr *extJSONValueReader) push(m mode, t bsontype.Type) { + ejvr.advanceFrame() + + ejvr.stack[ejvr.frame].mode = m + ejvr.stack[ejvr.frame].vType = t +} + +func (ejvr *extJSONValueReader) pop() { + switch ejvr.stack[ejvr.frame].mode { + case mElement, mValue: + ejvr.frame-- + case mDocument, mArray, mCodeWithScope: + ejvr.frame -= 2 // we pop twice to jump over the vrElement: vrDocument -> vrElement -> vrDocument/TopLevel/etc... + } +} + +func (ejvr *extJSONValueReader) skipObject() { + // read entire object until depth returns to 0 (last ending } or ] seen) + depth := 1 + for depth > 0 { + ejvr.p.advanceState() + + // If object is empty, raise depth and continue. When emptyObject is true, the + // parser has already read both the opening and closing brackets of an empty + // object ("{}"), so the next valid token will be part of the parent document, + // not part of the nested document. + // + // If there is a comma, there are remaining fields, emptyObject must be set back + // to false, and comma must be skipped with advanceState(). + if ejvr.p.emptyObject { + if ejvr.p.s == jpsSawComma { + ejvr.p.emptyObject = false + ejvr.p.advanceState() + } + depth-- + continue + } + + switch ejvr.p.s { + case jpsSawBeginObject, jpsSawBeginArray: + depth++ + case jpsSawEndObject, jpsSawEndArray: + depth-- + } + } +} + +func (ejvr *extJSONValueReader) invalidTransitionErr(destination mode, name string, modes []mode) error { + te := TransitionError{ + name: name, + current: ejvr.stack[ejvr.frame].mode, + destination: destination, + modes: modes, + action: "read", + } + if ejvr.frame != 0 { + te.parent = ejvr.stack[ejvr.frame-1].mode + } + return te +} + +func (ejvr *extJSONValueReader) typeError(t bsontype.Type) error { + return fmt.Errorf("positioned on %s, but attempted to read %s", ejvr.stack[ejvr.frame].vType, t) +} + +func (ejvr *extJSONValueReader) ensureElementValue(t bsontype.Type, destination mode, callerName string, addModes ...mode) error { + switch ejvr.stack[ejvr.frame].mode { + case mElement, mValue: + if ejvr.stack[ejvr.frame].vType != t { + return ejvr.typeError(t) + } + default: + modes := []mode{mElement, mValue} + if addModes != nil { + modes = append(modes, addModes...) + } + return ejvr.invalidTransitionErr(destination, callerName, modes) + } + + return nil +} + +func (ejvr *extJSONValueReader) Type() bsontype.Type { + return ejvr.stack[ejvr.frame].vType +} + +func (ejvr *extJSONValueReader) Skip() error { + switch ejvr.stack[ejvr.frame].mode { + case mElement, mValue: + default: + return ejvr.invalidTransitionErr(0, "Skip", []mode{mElement, mValue}) + } + + defer ejvr.pop() + + t := ejvr.stack[ejvr.frame].vType + switch t { + case bsontype.Array, bsontype.EmbeddedDocument, bsontype.CodeWithScope: + // read entire array, doc or CodeWithScope + ejvr.skipObject() + default: + _, err := ejvr.p.readValue(t) + if err != nil { + return err + } + } + + return nil +} + +func (ejvr *extJSONValueReader) ReadArray() (ArrayReader, error) { + switch ejvr.stack[ejvr.frame].mode { + case mTopLevel: // allow reading array from top level + case mArray: + return ejvr, nil + default: + if err := ejvr.ensureElementValue(bsontype.Array, mArray, "ReadArray", mTopLevel, mArray); err != nil { + return nil, err + } + } + + ejvr.pushArray() + + return ejvr, nil +} + +func (ejvr *extJSONValueReader) ReadBinary() (b []byte, btype byte, err error) { + if err := ejvr.ensureElementValue(bsontype.Binary, 0, "ReadBinary"); err != nil { + return nil, 0, err + } + + v, err := ejvr.p.readValue(bsontype.Binary) + if err != nil { + return nil, 0, err + } + + b, btype, err = v.parseBinary() + + ejvr.pop() + return b, btype, err +} + +func (ejvr *extJSONValueReader) ReadBoolean() (bool, error) { + if err := ejvr.ensureElementValue(bsontype.Boolean, 0, "ReadBoolean"); err != nil { + return false, err + } + + v, err := ejvr.p.readValue(bsontype.Boolean) + if err != nil { + return false, err + } + + if v.t != bsontype.Boolean { + return false, fmt.Errorf("expected type bool, but got type %s", v.t) + } + + ejvr.pop() + return v.v.(bool), nil +} + +func (ejvr *extJSONValueReader) ReadDocument() (DocumentReader, error) { + switch ejvr.stack[ejvr.frame].mode { + case mTopLevel: + return ejvr, nil + case mElement, mValue: + if ejvr.stack[ejvr.frame].vType != bsontype.EmbeddedDocument { + return nil, ejvr.typeError(bsontype.EmbeddedDocument) + } + + ejvr.pushDocument() + return ejvr, nil + default: + return nil, ejvr.invalidTransitionErr(mDocument, "ReadDocument", []mode{mTopLevel, mElement, mValue}) + } +} + +func (ejvr *extJSONValueReader) ReadCodeWithScope() (code string, dr DocumentReader, err error) { + if err = ejvr.ensureElementValue(bsontype.CodeWithScope, 0, "ReadCodeWithScope"); err != nil { + return "", nil, err + } + + v, err := ejvr.p.readValue(bsontype.CodeWithScope) + if err != nil { + return "", nil, err + } + + code, err = v.parseJavascript() + + ejvr.pushCodeWithScope() + return code, ejvr, err +} + +func (ejvr *extJSONValueReader) ReadDBPointer() (ns string, oid primitive.ObjectID, err error) { + if err = ejvr.ensureElementValue(bsontype.DBPointer, 0, "ReadDBPointer"); err != nil { + return "", primitive.NilObjectID, err + } + + v, err := ejvr.p.readValue(bsontype.DBPointer) + if err != nil { + return "", primitive.NilObjectID, err + } + + ns, oid, err = v.parseDBPointer() + + ejvr.pop() + return ns, oid, err +} + +func (ejvr *extJSONValueReader) ReadDateTime() (int64, error) { + if err := ejvr.ensureElementValue(bsontype.DateTime, 0, "ReadDateTime"); err != nil { + return 0, err + } + + v, err := ejvr.p.readValue(bsontype.DateTime) + if err != nil { + return 0, err + } + + d, err := v.parseDateTime() + + ejvr.pop() + return d, err +} + +func (ejvr *extJSONValueReader) ReadDecimal128() (primitive.Decimal128, error) { + if err := ejvr.ensureElementValue(bsontype.Decimal128, 0, "ReadDecimal128"); err != nil { + return primitive.Decimal128{}, err + } + + v, err := ejvr.p.readValue(bsontype.Decimal128) + if err != nil { + return primitive.Decimal128{}, err + } + + d, err := v.parseDecimal128() + + ejvr.pop() + return d, err +} + +func (ejvr *extJSONValueReader) ReadDouble() (float64, error) { + if err := ejvr.ensureElementValue(bsontype.Double, 0, "ReadDouble"); err != nil { + return 0, err + } + + v, err := ejvr.p.readValue(bsontype.Double) + if err != nil { + return 0, err + } + + d, err := v.parseDouble() + + ejvr.pop() + return d, err +} + +func (ejvr *extJSONValueReader) ReadInt32() (int32, error) { + if err := ejvr.ensureElementValue(bsontype.Int32, 0, "ReadInt32"); err != nil { + return 0, err + } + + v, err := ejvr.p.readValue(bsontype.Int32) + if err != nil { + return 0, err + } + + i, err := v.parseInt32() + + ejvr.pop() + return i, err +} + +func (ejvr *extJSONValueReader) ReadInt64() (int64, error) { + if err := ejvr.ensureElementValue(bsontype.Int64, 0, "ReadInt64"); err != nil { + return 0, err + } + + v, err := ejvr.p.readValue(bsontype.Int64) + if err != nil { + return 0, err + } + + i, err := v.parseInt64() + + ejvr.pop() + return i, err +} + +func (ejvr *extJSONValueReader) ReadJavascript() (code string, err error) { + if err = ejvr.ensureElementValue(bsontype.JavaScript, 0, "ReadJavascript"); err != nil { + return "", err + } + + v, err := ejvr.p.readValue(bsontype.JavaScript) + if err != nil { + return "", err + } + + code, err = v.parseJavascript() + + ejvr.pop() + return code, err +} + +func (ejvr *extJSONValueReader) ReadMaxKey() error { + if err := ejvr.ensureElementValue(bsontype.MaxKey, 0, "ReadMaxKey"); err != nil { + return err + } + + v, err := ejvr.p.readValue(bsontype.MaxKey) + if err != nil { + return err + } + + err = v.parseMinMaxKey("max") + + ejvr.pop() + return err +} + +func (ejvr *extJSONValueReader) ReadMinKey() error { + if err := ejvr.ensureElementValue(bsontype.MinKey, 0, "ReadMinKey"); err != nil { + return err + } + + v, err := ejvr.p.readValue(bsontype.MinKey) + if err != nil { + return err + } + + err = v.parseMinMaxKey("min") + + ejvr.pop() + return err +} + +func (ejvr *extJSONValueReader) ReadNull() error { + if err := ejvr.ensureElementValue(bsontype.Null, 0, "ReadNull"); err != nil { + return err + } + + v, err := ejvr.p.readValue(bsontype.Null) + if err != nil { + return err + } + + if v.t != bsontype.Null { + return fmt.Errorf("expected type null but got type %s", v.t) + } + + ejvr.pop() + return nil +} + +func (ejvr *extJSONValueReader) ReadObjectID() (primitive.ObjectID, error) { + if err := ejvr.ensureElementValue(bsontype.ObjectID, 0, "ReadObjectID"); err != nil { + return primitive.ObjectID{}, err + } + + v, err := ejvr.p.readValue(bsontype.ObjectID) + if err != nil { + return primitive.ObjectID{}, err + } + + oid, err := v.parseObjectID() + + ejvr.pop() + return oid, err +} + +func (ejvr *extJSONValueReader) ReadRegex() (pattern string, options string, err error) { + if err = ejvr.ensureElementValue(bsontype.Regex, 0, "ReadRegex"); err != nil { + return "", "", err + } + + v, err := ejvr.p.readValue(bsontype.Regex) + if err != nil { + return "", "", err + } + + pattern, options, err = v.parseRegex() + + ejvr.pop() + return pattern, options, err +} + +func (ejvr *extJSONValueReader) ReadString() (string, error) { + if err := ejvr.ensureElementValue(bsontype.String, 0, "ReadString"); err != nil { + return "", err + } + + v, err := ejvr.p.readValue(bsontype.String) + if err != nil { + return "", err + } + + if v.t != bsontype.String { + return "", fmt.Errorf("expected type string but got type %s", v.t) + } + + ejvr.pop() + return v.v.(string), nil +} + +func (ejvr *extJSONValueReader) ReadSymbol() (symbol string, err error) { + if err = ejvr.ensureElementValue(bsontype.Symbol, 0, "ReadSymbol"); err != nil { + return "", err + } + + v, err := ejvr.p.readValue(bsontype.Symbol) + if err != nil { + return "", err + } + + symbol, err = v.parseSymbol() + + ejvr.pop() + return symbol, err +} + +func (ejvr *extJSONValueReader) ReadTimestamp() (t uint32, i uint32, err error) { + if err = ejvr.ensureElementValue(bsontype.Timestamp, 0, "ReadTimestamp"); err != nil { + return 0, 0, err + } + + v, err := ejvr.p.readValue(bsontype.Timestamp) + if err != nil { + return 0, 0, err + } + + t, i, err = v.parseTimestamp() + + ejvr.pop() + return t, i, err +} + +func (ejvr *extJSONValueReader) ReadUndefined() error { + if err := ejvr.ensureElementValue(bsontype.Undefined, 0, "ReadUndefined"); err != nil { + return err + } + + v, err := ejvr.p.readValue(bsontype.Undefined) + if err != nil { + return err + } + + err = v.parseUndefined() + + ejvr.pop() + return err +} + +func (ejvr *extJSONValueReader) ReadElement() (string, ValueReader, error) { + switch ejvr.stack[ejvr.frame].mode { + case mTopLevel, mDocument, mCodeWithScope: + default: + return "", nil, ejvr.invalidTransitionErr(mElement, "ReadElement", []mode{mTopLevel, mDocument, mCodeWithScope}) + } + + name, t, err := ejvr.p.readKey() + + if err != nil { + if err == ErrEOD { + if ejvr.stack[ejvr.frame].mode == mCodeWithScope { + _, err := ejvr.p.peekType() + if err != nil { + return "", nil, err + } + } + + ejvr.pop() + } + + return "", nil, err + } + + ejvr.push(mElement, t) + return name, ejvr, nil +} + +func (ejvr *extJSONValueReader) ReadValue() (ValueReader, error) { + switch ejvr.stack[ejvr.frame].mode { + case mArray: + default: + return nil, ejvr.invalidTransitionErr(mValue, "ReadValue", []mode{mArray}) + } + + t, err := ejvr.p.peekType() + if err != nil { + if err == ErrEOA { + ejvr.pop() + } + + return nil, err + } + + ejvr.push(mValue, t) + return ejvr, nil +} diff --git a/mongo/bson/bsonrw/extjson_reader_test.go b/mongo/bson/bsonrw/extjson_reader_test.go new file mode 100644 index 0000000..8a9f0cc --- /dev/null +++ b/mongo/bson/bsonrw/extjson_reader_test.go @@ -0,0 +1,168 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 + +package bsonrw + +import ( + "fmt" + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +func TestExtJSONReader(t *testing.T) { + t.Run("ReadDocument", func(t *testing.T) { + t.Run("EmbeddedDocument", func(t *testing.T) { + ejvr := &extJSONValueReader{ + stack: []ejvrState{ + {mode: mTopLevel}, + {mode: mElement, vType: bsontype.Boolean}, + }, + frame: 1, + } + + ejvr.stack[1].mode = mArray + wanterr := ejvr.invalidTransitionErr(mDocument, "ReadDocument", []mode{mTopLevel, mElement, mValue}) + _, err := ejvr.ReadDocument() + if err == nil || err.Error() != wanterr.Error() { + t.Errorf("Incorrect returned error. got %v; want %v", err, wanterr) + } + + }) + }) + + t.Run("invalid transition", func(t *testing.T) { + t.Run("Skip", func(t *testing.T) { + ejvr := &extJSONValueReader{stack: []ejvrState{{mode: mTopLevel}}} + wanterr := (&extJSONValueReader{stack: []ejvrState{{mode: mTopLevel}}}).invalidTransitionErr(0, "Skip", []mode{mElement, mValue}) + goterr := ejvr.Skip() + if !cmp.Equal(goterr, wanterr, cmp.Comparer(compareErrors)) { + t.Errorf("Expected correct invalid transition error. got %v; want %v", goterr, wanterr) + } + }) + }) +} + +func TestReadMultipleTopLevelDocuments(t *testing.T) { + testCases := []struct { + name string + input string + expected [][]byte + }{ + { + "single top-level document", + "{\"foo\":1}", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + }, + }, + { + "single top-level document with leading and trailing whitespace", + "\n\n {\"foo\":1} \n", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + }, + }, + { + "two top-level documents", + "{\"foo\":1}{\"foo\":2}", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x02, 0x00, 0x00, 0x00, 0x00}, + }, + }, + { + "two top-level documents with leading and trailing whitespace and whitespace separation ", + "\n\n {\"foo\":1}\n{\"foo\":2}\n ", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x02, 0x00, 0x00, 0x00, 0x00}, + }, + }, + { + "top-level array with single document", + "[{\"foo\":1}]", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + }, + }, + { + "top-level array with 2 documents", + "[{\"foo\":1},{\"foo\":2}]", + [][]byte{ + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, + {0x0E, 0x00, 0x00, 0x00, 0x10, 'f', 'o', 'o', 0x00, 0x02, 0x00, 0x00, 0x00, 0x00}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := strings.NewReader(tc.input) + vr, err := NewExtJSONValueReader(r, false) + if err != nil { + t.Fatalf("expected no error, but got %v", err) + } + + actual, err := readAllDocuments(vr) + if err != nil { + t.Fatalf("expected no error, but got %v", err) + } + + if diff := cmp.Diff(tc.expected, actual); diff != "" { + t.Fatalf("expected does not match actual: %v", diff) + } + }) + } +} + +func readAllDocuments(vr ValueReader) ([][]byte, error) { + c := NewCopier() + var actual [][]byte + + switch vr.Type() { + case bsontype.EmbeddedDocument: + for { + result, err := c.CopyDocumentToBytes(vr) + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + actual = append(actual, result) + } + case bsontype.Array: + ar, err := vr.ReadArray() + if err != nil { + return nil, err + } + for { + evr, err := ar.ReadValue() + if err != nil { + if err == ErrEOA { + break + } + return nil, err + } + + result, err := c.CopyDocumentToBytes(evr) + if err != nil { + return nil, err + } + + actual = append(actual, result) + } + default: + return nil, fmt.Errorf("expected an array or a document, but got %s", vr.Type()) + } + + return actual, nil +} diff --git a/mongo/bson/bsonrw/extjson_tables.go b/mongo/bson/bsonrw/extjson_tables.go new file mode 100644 index 0000000..ba39c96 --- /dev/null +++ b/mongo/bson/bsonrw/extjson_tables.go @@ -0,0 +1,223 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// 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 +// +// Based on github.com/golang/go by The Go Authors +// See THIRD-PARTY-NOTICES for original license terms. + +package bsonrw + +import "unicode/utf8" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML