summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:02:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:02:14 +0000
commite61c5fa98419989a61b5ca4eb7749acbd37e0af6 (patch)
treeffe04b0283921bd40489aaa74dcee1f68b7b6b35
parentInitial commit. (diff)
downloadgolang-toml-upstream.tar.xz
golang-toml-upstream.zip
Adding upstream version 1.3.2.upstream/1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/workflows/test.yml31
-rw-r--r--.gitignore2
-rw-r--r--COPYING21
-rw-r--r--README.md120
-rw-r--r--_example/example.go106
-rw-r--r--_example/example.toml53
-rw-r--r--bench_test.go218
-rw-r--r--cmd/toml-test-decoder/COPYING21
-rw-r--r--cmd/toml-test-decoder/README.md6
-rw-r--r--cmd/toml-test-decoder/main.go43
-rw-r--r--cmd/toml-test-encoder/COPYING21
-rw-r--r--cmd/toml-test-encoder/README.md6
-rw-r--r--cmd/toml-test-encoder/main.go46
-rw-r--r--cmd/tomlv/COPYING21
-rw-r--r--cmd/tomlv/README.md14
-rw-r--r--cmd/tomlv/main.go56
-rw-r--r--decode.go602
-rw-r--r--decode_go116.go19
-rw-r--r--decode_go116_test.go29
-rw-r--r--decode_test.go1238
-rw-r--r--deprecated.go29
-rw-r--r--doc.go11
-rw-r--r--encode.go759
-rw-r--r--encode_test.go1326
-rw-r--r--error.go279
-rw-r--r--error_test.go245
-rw-r--r--example_test.go387
-rw-r--r--fuzz_test.go81
-rw-r--r--go.mod3
-rw-r--r--internal/tag/add.go76
-rw-r--r--internal/tag/rm.go111
-rw-r--r--internal/toml-test/.gogo-release1
-rw-r--r--internal/toml-test/COPYING21
-rw-r--r--internal/toml-test/README.md271
-rw-r--r--internal/toml-test/json.go258
-rw-r--r--internal/toml-test/runner.go427
-rw-r--r--internal/toml-test/tests/.gitattributes1
-rw-r--r--internal/toml-test/tests/invalid/array/double-comma-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/double-comma-2.toml2
-rw-r--r--internal/toml-test/tests/invalid/array/extending-table.toml6
-rw-r--r--internal/toml-test/tests/invalid/array/missing-separator.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/no-close-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/no-close-table-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/no-close-table.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/no-close.toml1
-rw-r--r--internal/toml-test/tests/invalid/array/tables-1.toml4
-rw-r--r--internal/toml-test/tests/invalid/array/tables-2.toml10
-rw-r--r--internal/toml-test/tests/invalid/array/text-after-array-entries.toml4
-rw-r--r--internal/toml-test/tests/invalid/array/text-before-array-separator.toml4
-rw-r--r--internal/toml-test/tests/invalid/array/text-in-array.toml5
-rw-r--r--internal/toml-test/tests/invalid/bool/almost-false-with-extra.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/almost-false.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/almost-true-with-extra.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/almost-true.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/just-f.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/just-t.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/mixed-case.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/starting-same-false.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/starting-same-true.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/wrong-case-false.toml1
-rw-r--r--internal/toml-test/tests/invalid/bool/wrong-case-true.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/bare-cr.toml2
-rw-r--r--internal/toml-test/tests/invalid/control/bare-formfeed.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/bare-null.tomlbin0 -> 27 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/bare-vertical-tab.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/comment-cr.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/comment-del.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/comment-lf.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/comment-null.tomlbin0 -> 26 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/comment-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/control.multi33
-rw-r--r--internal/toml-test/tests/invalid/control/multi-del.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/multi-lf.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/multi-null.tomlbin0 -> 25 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/multi-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawmulti-del.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawmulti-lf.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawmulti-null.tomlbin0 -> 28 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/rawmulti-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawstring-del.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawstring-lf.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/rawstring-null.tomlbin0 -> 25 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/rawstring-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/string-bs.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/string-del.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/string-lf.toml1
-rw-r--r--internal/toml-test/tests/invalid/control/string-null.tomlbin0 -> 22 bytes
-rw-r--r--internal/toml-test/tests/invalid/control/string-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/datetime/hour-over.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/mday-over.toml3
-rw-r--r--internal/toml-test/tests/invalid/datetime/mday-under.toml3
-rw-r--r--internal/toml-test/tests/invalid/datetime/minute-over.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/month-over.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/month-under.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/no-leads-with-milli.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/no-leads.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/no-secs.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/no-t.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/second-over.toml3
-rw-r--r--internal/toml-test/tests/invalid/datetime/time-no-leads-2.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/time-no-leads.toml2
-rw-r--r--internal/toml-test/tests/invalid/datetime/trailing-t.toml2
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-codepoint.toml1
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-at-end.toml5
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-in-comment.toml1
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline-literal.toml2
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline.toml2
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-in-string-literal.toml2
-rw-r--r--internal/toml-test/tests/invalid/encoding/bad-utf8-in-string.toml2
-rw-r--r--internal/toml-test/tests/invalid/encoding/bom-not-at-start-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/encoding/bom-not-at-start-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/encoding/utf16-bom.tomlbin0 -> 38 bytes
-rw-r--r--internal/toml-test/tests/invalid/encoding/utf16.tomlbin0 -> 42 bytes
-rw-r--r--internal/toml-test/tests/invalid/float/double-point-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/double-point-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-double-e-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-double-e-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-double-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-leading-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-point-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-point-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/exp-trailing-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/float.multi40
-rw-r--r--internal/toml-test/tests/invalid/float/inf-capital.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/inf-incomplete-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/inf-incomplete-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/inf-incomplete-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/inf_underscore.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-point-neg.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-point-plus.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-point.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-zero-neg.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-zero-plus.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/leading-zero.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/nan-capital.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/nan-incomplete-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/nan-incomplete-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/nan-incomplete-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/nan_underscore.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/trailing-point-min.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/trailing-point-plus.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/trailing-point.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/trailing-us-exp.toml4
-rw-r--r--internal/toml-test/tests/invalid/float/trailing-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/us-after-point.toml1
-rw-r--r--internal/toml-test/tests/invalid/float/us-before-point.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/add.toml3
-rw-r--r--internal/toml-test/tests/invalid/inline-table/bad-key-syntax.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/dotted-key-conflict.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/double-comma.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/duplicate-key.toml2
-rw-r--r--internal/toml-test/tests/invalid/inline-table/empty.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/linebreak-1.toml4
-rw-r--r--internal/toml-test/tests/invalid/inline-table/linebreak-2.toml2
-rw-r--r--internal/toml-test/tests/invalid/inline-table/linebreak-3.toml2
-rw-r--r--internal/toml-test/tests/invalid/inline-table/linebreak-4.toml4
-rw-r--r--internal/toml-test/tests/invalid/inline-table/nested_key_conflict.toml2
-rw-r--r--internal/toml-test/tests/invalid/inline-table/no-comma.toml1
-rw-r--r--internal/toml-test/tests/invalid/inline-table/overwrite.toml3
-rw-r--r--internal/toml-test/tests/invalid/inline-table/trailing-comma.toml3
-rw-r--r--internal/toml-test/tests/invalid/integer/capital-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/capital-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/capital-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/double-sign-nex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/double-sign-plus.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/double-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/incomplete-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/incomplete-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/incomplete-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/integer.multi41
-rw-r--r--internal/toml-test/tests/invalid/integer/invalid-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/invalid-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/invalid-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-us-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-us-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-us-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-sign-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-sign-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/leading-zero-sign-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/negative-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/negative-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/negative-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/positive-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/positive-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/positive-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/text-after-integer.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/trailing-us-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/trailing-us-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/trailing-us-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/trailing-us.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/us-after-bin.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/us-after-hex.toml1
-rw-r--r--internal/toml-test/tests/invalid/integer/us-after-oct.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/after-array.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/after-table.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/after-value.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/bare-invalid-character.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/dotted-redefine-table.toml4
-rw-r--r--internal/toml-test/tests/invalid/key/duplicate-keys.toml2
-rw-r--r--internal/toml-test/tests/invalid/key/duplicate.toml3
-rw-r--r--internal/toml-test/tests/invalid/key/empty.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/escape.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/hash.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/multiline.toml2
-rw-r--r--internal/toml-test/tests/invalid/key/newline.toml2
-rw-r--r--internal/toml-test/tests/invalid/key/no-eol.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/open-bracket.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/partial-quoted.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/quoted-unclosed-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/quoted-unclosed-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/single-open-bracket.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/space.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/special-character.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/start-bracket.toml3
-rw-r--r--internal/toml-test/tests/invalid/key/start-dot.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/two-equals.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/two-equals2.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/two-equals3.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/without-value-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/without-value-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/without-value-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/key/without-value-4.toml1
-rw-r--r--internal/toml-test/tests/invalid/spec/inline-table-2-0.toml3
-rw-r--r--internal/toml-test/tests/invalid/spec/inline-table-3-0.toml3
-rw-r--r--internal/toml-test/tests/invalid/spec/key-value-pair-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/spec/keys-2.toml3
-rw-r--r--internal/toml-test/tests/invalid/spec/string-4-0.toml7
-rw-r--r--internal/toml-test/tests/invalid/spec/string-7-0.toml7
-rw-r--r--internal/toml-test/tests/invalid/spec/table-9-0.toml9
-rw-r--r--internal/toml-test/tests/invalid/spec/table-9-1.toml9
-rw-r--r--internal/toml-test/tests/invalid/string/bad-byte-escape.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-codepoint.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-concat.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-escape-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-escape-2.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc-3.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc-4.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc-5.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-hex-esc.multi4
-rw-r--r--internal/toml-test/tests/invalid/string/bad-multiline.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/bad-slash-escape.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-uni-esc-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-uni-esc-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-uni-esc-3.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/bad-uni-esc-4.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/bad-uni-esc-5.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-byte-escapes.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-multiline-quotes.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-multiline-unknown-escape.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/basic-unknown-escape.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/literal-multiline-quotes-1.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/literal-multiline-quotes-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/missing-quotes.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-bad-escape-1.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-bad-escape-2.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-bad-escape-3.toml3
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-escape-space.toml3
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-no-close-2.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-no-close.toml2
-rw-r--r--internal/toml-test/tests/invalid/string/multiline-quotes-1.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/no-close.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/text-after-string.toml1
-rw-r--r--internal/toml-test/tests/invalid/string/wrong-close.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/append-to-array-with-dotted-keys.toml4
-rw-r--r--internal/toml-test/tests/invalid/table/append-with-dotted-keys-1.toml17
-rw-r--r--internal/toml-test/tests/invalid/table/append-with-dotted-keys-2.toml8
-rw-r--r--internal/toml-test/tests/invalid/table/array-empty.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/array-implicit.toml14
-rw-r--r--internal/toml-test/tests/invalid/table/array-missing-bracket.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-key-dotted-array.toml4
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-key-dotted-table.toml4
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-key-dotted-table2.toml4
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-key-table.toml5
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-table-array.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate-table-array2.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/duplicate.toml5
-rw-r--r--internal/toml-test/tests/invalid/table/empty-implicit-table.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/empty.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/equals-sign.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/llbrace.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/nested-brackets-close.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/nested-brackets-open.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/quoted-no-close.toml2
-rw-r--r--internal/toml-test/tests/invalid/table/redefine.toml6
-rw-r--r--internal/toml-test/tests/invalid/table/rrbrace.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/text-after-table.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/whitespace.toml1
-rw-r--r--internal/toml-test/tests/invalid/table/with-pound.toml2
-rw-r--r--internal/toml-test/tests/valid/array/array.json68
-rw-r--r--internal/toml-test/tests/valid/array/array.toml12
-rw-r--r--internal/toml-test/tests/valid/array/bool.json12
-rw-r--r--internal/toml-test/tests/valid/array/bool.toml1
-rw-r--r--internal/toml-test/tests/valid/array/empty.json11
-rw-r--r--internal/toml-test/tests/valid/array/empty.toml1
-rw-r--r--internal/toml-test/tests/valid/array/hetergeneous.json34
-rw-r--r--internal/toml-test/tests/valid/array/hetergeneous.toml1
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-array.json14
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-array.toml1
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-float.json12
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-float.toml1
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-string.json12
-rw-r--r--internal/toml-test/tests/valid/array/mixed-int-string.toml1
-rw-r--r--internal/toml-test/tests/valid/array/mixed-string-table.json38
-rw-r--r--internal/toml-test/tests/valid/array/mixed-string-table.toml11
-rw-r--r--internal/toml-test/tests/valid/array/nested-double.json28
-rw-r--r--internal/toml-test/tests/valid/array/nested-double.toml6
-rw-r--r--internal/toml-test/tests/valid/array/nested-inline-table.json7
-rw-r--r--internal/toml-test/tests/valid/array/nested-inline-table.toml1
-rw-r--r--internal/toml-test/tests/valid/array/nested.json16
-rw-r--r--internal/toml-test/tests/valid/array/nested.toml1
-rw-r--r--internal/toml-test/tests/valid/array/nospaces.json16
-rw-r--r--internal/toml-test/tests/valid/array/nospaces.toml1
-rw-r--r--internal/toml-test/tests/valid/array/string-quote-comma-2.json8
-rw-r--r--internal/toml-test/tests/valid/array/string-quote-comma-2.toml1
-rw-r--r--internal/toml-test/tests/valid/array/string-quote-comma.json12
-rw-r--r--internal/toml-test/tests/valid/array/string-quote-comma.toml4
-rw-r--r--internal/toml-test/tests/valid/array/string-with-comma.json12
-rw-r--r--internal/toml-test/tests/valid/array/string-with-comma.toml4
-rw-r--r--internal/toml-test/tests/valid/array/strings.json20
-rw-r--r--internal/toml-test/tests/valid/array/strings.toml1
-rw-r--r--internal/toml-test/tests/valid/array/table-array-string-backslash.json10
-rw-r--r--internal/toml-test/tests/valid/array/table-array-string-backslash.toml1
-rw-r--r--internal/toml-test/tests/valid/bool/bool.json10
-rw-r--r--internal/toml-test/tests/valid/bool/bool.toml2
-rw-r--r--internal/toml-test/tests/valid/comment/at-eof.json6
-rw-r--r--internal/toml-test/tests/valid/comment/at-eof.toml2
-rw-r--r--internal/toml-test/tests/valid/comment/at-eof2.json6
-rw-r--r--internal/toml-test/tests/valid/comment/at-eof2.toml2
-rw-r--r--internal/toml-test/tests/valid/comment/everywhere.json26
-rw-r--r--internal/toml-test/tests/valid/comment/everywhere.toml28
-rw-r--r--internal/toml-test/tests/valid/comment/noeol.json1
-rw-r--r--internal/toml-test/tests/valid/comment/noeol.toml1
-rw-r--r--internal/toml-test/tests/valid/comment/nonascii.json1
-rw-r--r--internal/toml-test/tests/valid/comment/nonascii.toml1
-rw-r--r--internal/toml-test/tests/valid/comment/tricky.json98
-rw-r--r--internal/toml-test/tests/valid/comment/tricky.toml31
-rw-r--r--internal/toml-test/tests/valid/datetime/datetime.json10
-rw-r--r--internal/toml-test/tests/valid/datetime/datetime.toml2
-rw-r--r--internal/toml-test/tests/valid/datetime/local-date.json6
-rw-r--r--internal/toml-test/tests/valid/datetime/local-date.toml1
-rw-r--r--internal/toml-test/tests/valid/datetime/local-time.json10
-rw-r--r--internal/toml-test/tests/valid/datetime/local-time.toml2
-rw-r--r--internal/toml-test/tests/valid/datetime/local.json14
-rw-r--r--internal/toml-test/tests/valid/datetime/local.toml3
-rw-r--r--internal/toml-test/tests/valid/datetime/milliseconds.json18
-rw-r--r--internal/toml-test/tests/valid/datetime/milliseconds.toml4
-rw-r--r--internal/toml-test/tests/valid/datetime/no-seconds.json18
-rw-r--r--internal/toml-test/tests/valid/datetime/no-seconds.toml5
-rw-r--r--internal/toml-test/tests/valid/datetime/timezone.json18
-rw-r--r--internal/toml-test/tests/valid/datetime/timezone.toml4
-rw-r--r--internal/toml-test/tests/valid/empty-file.json1
-rw-r--r--internal/toml-test/tests/valid/empty-file.toml0
-rw-r--r--internal/toml-test/tests/valid/example.json26
-rw-r--r--internal/toml-test/tests/valid/example.toml5
-rw-r--r--internal/toml-test/tests/valid/float/exponent.json34
-rw-r--r--internal/toml-test/tests/valid/float/exponent.toml8
-rw-r--r--internal/toml-test/tests/valid/float/float.json18
-rw-r--r--internal/toml-test/tests/valid/float/float.toml4
-rw-r--r--internal/toml-test/tests/valid/float/inf-and-nan.json26
-rw-r--r--internal/toml-test/tests/valid/float/inf-and-nan.toml8
-rw-r--r--internal/toml-test/tests/valid/float/long.json10
-rw-r--r--internal/toml-test/tests/valid/float/long.toml2
-rw-r--r--internal/toml-test/tests/valid/float/underscore.json14
-rw-r--r--internal/toml-test/tests/valid/float/underscore.toml3
-rw-r--r--internal/toml-test/tests/valid/float/zero.json30
-rw-r--r--internal/toml-test/tests/valid/float/zero.toml7
-rw-r--r--internal/toml-test/tests/valid/implicit-and-explicit-after.json16
-rw-r--r--internal/toml-test/tests/valid/implicit-and-explicit-after.toml5
-rw-r--r--internal/toml-test/tests/valid/implicit-and-explicit-before.json16
-rw-r--r--internal/toml-test/tests/valid/implicit-and-explicit-before.toml5
-rw-r--r--internal/toml-test/tests/valid/implicit-groups.json12
-rw-r--r--internal/toml-test/tests/valid/implicit-groups.toml2
-rw-r--r--internal/toml-test/tests/valid/inline-table/array.json34
-rw-r--r--internal/toml-test/tests/valid/inline-table/array.toml3
-rw-r--r--internal/toml-test/tests/valid/inline-table/bool.json12
-rw-r--r--internal/toml-test/tests/valid/inline-table/bool.toml1
-rw-r--r--internal/toml-test/tests/valid/inline-table/empty.json30
-rw-r--r--internal/toml-test/tests/valid/inline-table/empty.toml6
-rw-r--r--internal/toml-test/tests/valid/inline-table/end-in-bool.json16
-rw-r--r--internal/toml-test/tests/valid/inline-table/end-in-bool.toml1
-rw-r--r--internal/toml-test/tests/valid/inline-table/inline-table.json48
-rw-r--r--internal/toml-test/tests/valid/inline-table/inline-table.toml5
-rw-r--r--internal/toml-test/tests/valid/inline-table/key-dotted.json140
-rw-r--r--internal/toml-test/tests/valid/inline-table/key-dotted.toml23
-rw-r--r--internal/toml-test/tests/valid/inline-table/multiline.json20
-rw-r--r--internal/toml-test/tests/valid/inline-table/multiline.toml4
-rw-r--r--internal/toml-test/tests/valid/inline-table/nest.json64
-rw-r--r--internal/toml-test/tests/valid/inline-table/nest.toml10
-rw-r--r--internal/toml-test/tests/valid/inline-table/newline.json50
-rw-r--r--internal/toml-test/tests/valid/inline-table/newline.toml24
-rw-r--r--internal/toml-test/tests/valid/integer/integer.json18
-rw-r--r--internal/toml-test/tests/valid/integer/integer.toml4
-rw-r--r--internal/toml-test/tests/valid/integer/literals.json38
-rw-r--r--internal/toml-test/tests/valid/integer/literals.toml11
-rw-r--r--internal/toml-test/tests/valid/integer/long.json10
-rw-r--r--internal/toml-test/tests/valid/integer/long.toml2
-rw-r--r--internal/toml-test/tests/valid/integer/underscore.json10
-rw-r--r--internal/toml-test/tests/valid/integer/underscore.toml2
-rw-r--r--internal/toml-test/tests/valid/integer/zero.json50
-rw-r--r--internal/toml-test/tests/valid/integer/zero.toml15
-rw-r--r--internal/toml-test/tests/valid/key/alphanum.json46
-rw-r--r--internal/toml-test/tests/valid/key/alphanum.toml14
-rw-r--r--internal/toml-test/tests/valid/key/case-sensitive.json38
-rw-r--r--internal/toml-test/tests/valid/key/case-sensitive.toml13
-rw-r--r--internal/toml-test/tests/valid/key/dotted-empty.json22
-rw-r--r--internal/toml-test/tests/valid/key/dotted-empty.toml4
-rw-r--r--internal/toml-test/tests/valid/key/dotted.json132
-rw-r--r--internal/toml-test/tests/valid/key/dotted.toml35
-rw-r--r--internal/toml-test/tests/valid/key/empty.json6
-rw-r--r--internal/toml-test/tests/valid/key/empty.toml1
-rw-r--r--internal/toml-test/tests/valid/key/equals-nospace.json6
-rw-r--r--internal/toml-test/tests/valid/key/equals-nospace.toml1
-rw-r--r--internal/toml-test/tests/valid/key/escapes.json24
-rw-r--r--internal/toml-test/tests/valid/key/escapes.toml10
-rw-r--r--internal/toml-test/tests/valid/key/numeric-dotted.json8
-rw-r--r--internal/toml-test/tests/valid/key/numeric-dotted.toml1
-rw-r--r--internal/toml-test/tests/valid/key/numeric.json6
-rw-r--r--internal/toml-test/tests/valid/key/numeric.toml1
-rw-r--r--internal/toml-test/tests/valid/key/quoted-dots.json32
-rw-r--r--internal/toml-test/tests/valid/key/quoted-dots.toml10
-rw-r--r--internal/toml-test/tests/valid/key/quoted-unicode.json22
-rw-r--r--internal/toml-test/tests/valid/key/quoted-unicode.toml7
-rw-r--r--internal/toml-test/tests/valid/key/space.json16
-rw-r--r--internal/toml-test/tests/valid/key/space.toml6
-rw-r--r--internal/toml-test/tests/valid/key/special-chars.json6
-rw-r--r--internal/toml-test/tests/valid/key/special-chars.toml1
-rw-r--r--internal/toml-test/tests/valid/key/special-word.json18
-rw-r--r--internal/toml-test/tests/valid/key/special-word.toml5
-rw-r--r--internal/toml-test/tests/valid/key/unicode.json18
-rw-r--r--internal/toml-test/tests/valid/key/unicode.toml6
-rw-r--r--internal/toml-test/tests/valid/newline-crlf.json10
-rw-r--r--internal/toml-test/tests/valid/newline-crlf.toml2
-rw-r--r--internal/toml-test/tests/valid/newline-lf.json10
-rw-r--r--internal/toml-test/tests/valid/newline-lf.toml2
-rw-r--r--internal/toml-test/tests/valid/spec-example-1-compact.json100
-rw-r--r--internal/toml-test/tests/valid/spec-example-1-compact.toml23
-rw-r--r--internal/toml-test/tests/valid/spec-example-1.json100
-rw-r--r--internal/toml-test/tests/valid/spec-example-1.toml33
-rw-r--r--internal/toml-test/tests/valid/spec/array-0.json146
-rw-r--r--internal/toml-test/tests/valid/spec/array-0.toml12
-rw-r--r--internal/toml-test/tests/valid/spec/array-1.json26
-rw-r--r--internal/toml-test/tests/valid/spec/array-1.toml8
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-0.json29
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-0.toml11
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-1.json48
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-1.toml19
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-2.json46
-rw-r--r--internal/toml-test/tests/valid/spec/array-of-tables-2.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/boolean-0.json10
-rw-r--r--internal/toml-test/tests/valid/spec/boolean-0.toml2
-rw-r--r--internal/toml-test/tests/valid/spec/comment-0.json10
-rw-r--r--internal/toml-test/tests/valid/spec/comment-0.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/float-0.json30
-rw-r--r--internal/toml-test/tests/valid/spec/float-0.toml12
-rw-r--r--internal/toml-test/tests/valid/spec/float-1.json6
-rw-r--r--internal/toml-test/tests/valid/spec/float-1.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/float-2.json26
-rw-r--r--internal/toml-test/tests/valid/spec/float-2.toml9
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-0.json30
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-0.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-1.json30
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-1.toml10
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-2.json10
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-2.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-3.json10
-rw-r--r--internal/toml-test/tests/valid/spec/inline-table-3.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/integer-0.json18
-rw-r--r--internal/toml-test/tests/valid/spec/integer-0.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/integer-1.json18
-rw-r--r--internal/toml-test/tests/valid/spec/integer-1.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/integer-2.json26
-rw-r--r--internal/toml-test/tests/valid/spec/integer-2.toml11
-rw-r--r--internal/toml-test/tests/valid/spec/key-value-pair-0.json6
-rw-r--r--internal/toml-test/tests/valid/spec/key-value-pair-0.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/keys-0.json18
-rw-r--r--internal/toml-test/tests/valid/spec/keys-0.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/keys-1.json22
-rw-r--r--internal/toml-test/tests/valid/spec/keys-1.toml5
-rw-r--r--internal/toml-test/tests/valid/spec/keys-3.json22
-rw-r--r--internal/toml-test/tests/valid/spec/keys-3.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/keys-4.json16
-rw-r--r--internal/toml-test/tests/valid/spec/keys-4.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/keys-5.json30
-rw-r--r--internal/toml-test/tests/valid/spec/keys-5.toml10
-rw-r--r--internal/toml-test/tests/valid/spec/keys-6.json30
-rw-r--r--internal/toml-test/tests/valid/spec/keys-6.toml9
-rw-r--r--internal/toml-test/tests/valid/spec/keys-7.json8
-rw-r--r--internal/toml-test/tests/valid/spec/keys-7.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/local-date-0.json6
-rw-r--r--internal/toml-test/tests/valid/spec/local-date-0.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/local-date-time-0.json10
-rw-r--r--internal/toml-test/tests/valid/spec/local-date-time-0.toml2
-rw-r--r--internal/toml-test/tests/valid/spec/local-time-0.json10
-rw-r--r--internal/toml-test/tests/valid/spec/local-time-0.toml2
-rw-r--r--internal/toml-test/tests/valid/spec/offset-date-time-0.json14
-rw-r--r--internal/toml-test/tests/valid/spec/offset-date-time-0.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/offset-date-time-1.json6
-rw-r--r--internal/toml-test/tests/valid/spec/offset-date-time-1.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/string-0.json6
-rw-r--r--internal/toml-test/tests/valid/spec/string-0.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/string-1.json6
-rw-r--r--internal/toml-test/tests/valid/spec/string-1.toml3
-rw-r--r--internal/toml-test/tests/valid/spec/string-2.json10
-rw-r--r--internal/toml-test/tests/valid/spec/string-2.toml5
-rw-r--r--internal/toml-test/tests/valid/spec/string-3.json14
-rw-r--r--internal/toml-test/tests/valid/spec/string-3.toml15
-rw-r--r--internal/toml-test/tests/valid/spec/string-4.json18
-rw-r--r--internal/toml-test/tests/valid/spec/string-4.toml7
-rw-r--r--internal/toml-test/tests/valid/spec/string-5.json18
-rw-r--r--internal/toml-test/tests/valid/spec/string-5.toml5
-rw-r--r--internal/toml-test/tests/valid/spec/string-6.json10
-rw-r--r--internal/toml-test/tests/valid/spec/string-6.toml7
-rw-r--r--internal/toml-test/tests/valid/spec/string-7.json14
-rw-r--r--internal/toml-test/tests/valid/spec/string-7.toml7
-rw-r--r--internal/toml-test/tests/valid/spec/table-0.json3
-rw-r--r--internal/toml-test/tests/valid/spec/table-0.toml1
-rw-r--r--internal/toml-test/tests/valid/spec/table-1.json22
-rw-r--r--internal/toml-test/tests/valid/spec/table-1.toml7
-rw-r--r--internal/toml-test/tests/valid/spec/table-2.json12
-rw-r--r--internal/toml-test/tests/valid/spec/table-2.toml2
-rw-r--r--internal/toml-test/tests/valid/spec/table-3.json22
-rw-r--r--internal/toml-test/tests/valid/spec/table-3.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/table-4.json9
-rw-r--r--internal/toml-test/tests/valid/spec/table-4.toml6
-rw-r--r--internal/toml-test/tests/valid/spec/table-5.json7
-rw-r--r--internal/toml-test/tests/valid/spec/table-5.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/table-6.json7
-rw-r--r--internal/toml-test/tests/valid/spec/table-6.toml4
-rw-r--r--internal/toml-test/tests/valid/spec/table-7.json20
-rw-r--r--internal/toml-test/tests/valid/spec/table-7.toml8
-rw-r--r--internal/toml-test/tests/valid/spec/table-8.json16
-rw-r--r--internal/toml-test/tests/valid/spec/table-8.toml7
-rw-r--r--internal/toml-test/tests/valid/spec/table-9.json22
-rw-r--r--internal/toml-test/tests/valid/spec/table-9.toml9
-rw-r--r--internal/toml-test/tests/valid/string/double-quote-escape.json6
-rw-r--r--internal/toml-test/tests/valid/string/double-quote-escape.toml1
-rw-r--r--internal/toml-test/tests/valid/string/empty.json6
-rw-r--r--internal/toml-test/tests/valid/string/empty.toml1
-rw-r--r--internal/toml-test/tests/valid/string/escape-esc.json6
-rw-r--r--internal/toml-test/tests/valid/string/escape-esc.toml1
-rw-r--r--internal/toml-test/tests/valid/string/escape-tricky.json30
-rw-r--r--internal/toml-test/tests/valid/string/escape-tricky.toml15
-rw-r--r--internal/toml-test/tests/valid/string/escaped-escape.json6
-rw-r--r--internal/toml-test/tests/valid/string/escaped-escape.toml1
-rw-r--r--internal/toml-test/tests/valid/string/escapes.json54
-rw-r--r--internal/toml-test/tests/valid/string/escapes.toml13
-rw-r--r--internal/toml-test/tests/valid/string/hex-escape.json34
-rw-r--r--internal/toml-test/tests/valid/string/hex-escape.toml21
-rw-r--r--internal/toml-test/tests/valid/string/multiline-escaped-crlf.json6
-rw-r--r--internal/toml-test/tests/valid/string/multiline-escaped-crlf.toml4
-rw-r--r--internal/toml-test/tests/valid/string/multiline-quotes.json46
-rw-r--r--internal/toml-test/tests/valid/string/multiline-quotes.toml19
-rw-r--r--internal/toml-test/tests/valid/string/multiline.json58
-rw-r--r--internal/toml-test/tests/valid/string/multiline.toml62
-rw-r--r--internal/toml-test/tests/valid/string/nl.json22
-rw-r--r--internal/toml-test/tests/valid/string/nl.toml6
-rw-r--r--internal/toml-test/tests/valid/string/quoted-unicode.json18
-rw-r--r--internal/toml-test/tests/valid/string/quoted-unicode.toml6
-rw-r--r--internal/toml-test/tests/valid/string/raw-multiline.json18
-rw-r--r--internal/toml-test/tests/valid/string/raw-multiline.toml18
-rw-r--r--internal/toml-test/tests/valid/string/raw.json34
-rw-r--r--internal/toml-test/tests/valid/string/raw.toml8
-rw-r--r--internal/toml-test/tests/valid/string/simple.json6
-rw-r--r--internal/toml-test/tests/valid/string/simple.toml1
-rw-r--r--internal/toml-test/tests/valid/string/unicode-escape.json10
-rw-r--r--internal/toml-test/tests/valid/string/unicode-escape.toml2
-rw-r--r--internal/toml-test/tests/valid/string/unicode-literal.json6
-rw-r--r--internal/toml-test/tests/valid/string/unicode-literal.toml1
-rw-r--r--internal/toml-test/tests/valid/string/with-pound.json10
-rw-r--r--internal/toml-test/tests/valid/string/with-pound.toml2
-rw-r--r--internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.json16
-rw-r--r--internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.toml5
-rw-r--r--internal/toml-test/tests/valid/table/array-implicit.json12
-rw-r--r--internal/toml-test/tests/valid/table/array-implicit.toml2
-rw-r--r--internal/toml-test/tests/valid/table/array-many.json34
-rw-r--r--internal/toml-test/tests/valid/table/array-many.toml11
-rw-r--r--internal/toml-test/tests/valid/table/array-nest.json44
-rw-r--r--internal/toml-test/tests/valid/table/array-nest.toml17
-rw-r--r--internal/toml-test/tests/valid/table/array-one.json14
-rw-r--r--internal/toml-test/tests/valid/table/array-one.toml3
-rw-r--r--internal/toml-test/tests/valid/table/array-table-array.json24
-rw-r--r--internal/toml-test/tests/valid/table/array-table-array.toml7
-rw-r--r--internal/toml-test/tests/valid/table/array-within-dotted.json18
-rw-r--r--internal/toml-test/tests/valid/table/array-within-dotted.toml5
-rw-r--r--internal/toml-test/tests/valid/table/empty-name.json22
-rw-r--r--internal/toml-test/tests/valid/table/empty-name.toml8
-rw-r--r--internal/toml-test/tests/valid/table/empty.json3
-rw-r--r--internal/toml-test/tests/valid/table/empty.toml1
-rw-r--r--internal/toml-test/tests/valid/table/keyword.json6
-rw-r--r--internal/toml-test/tests/valid/table/keyword.toml9
-rw-r--r--internal/toml-test/tests/valid/table/names.json30
-rw-r--r--internal/toml-test/tests/valid/table/names.toml9
-rw-r--r--internal/toml-test/tests/valid/table/no-eol.json3
-rw-r--r--internal/toml-test/tests/valid/table/no-eol.toml1
-rw-r--r--internal/toml-test/tests/valid/table/sub-empty.json5
-rw-r--r--internal/toml-test/tests/valid/table/sub-empty.toml2
-rw-r--r--internal/toml-test/tests/valid/table/sub.json20
-rw-r--r--internal/toml-test/tests/valid/table/sub.toml9
-rw-r--r--internal/toml-test/tests/valid/table/whitespace.json3
-rw-r--r--internal/toml-test/tests/valid/table/whitespace.toml1
-rw-r--r--internal/toml-test/tests/valid/table/with-literal-string.json12
-rw-r--r--internal/toml-test/tests/valid/table/with-literal-string.toml4
-rw-r--r--internal/toml-test/tests/valid/table/with-pound.json8
-rw-r--r--internal/toml-test/tests/valid/table/with-pound.toml2
-rw-r--r--internal/toml-test/tests/valid/table/with-single-quotes.json12
-rw-r--r--internal/toml-test/tests/valid/table/with-single-quotes.toml4
-rw-r--r--internal/toml-test/tests/valid/table/without-super.json9
-rw-r--r--internal/toml-test/tests/valid/table/without-super.toml5
-rw-r--r--internal/toml-test/toml.go136
-rw-r--r--internal/toml-test/version.go34
-rw-r--r--internal/tz.go36
-rw-r--r--lex.go1283
-rw-r--r--meta.go121
-rw-r--r--parse.go811
-rw-r--r--testdata/ja-JP.toml1221
-rw-r--r--toml_test.go514
-rw-r--r--type_fields.go242
-rw-r--r--type_toml.go70
630 files changed, 16577 insertions, 0 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..bb69c2a
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: arp242
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..ac0a351
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,31 @@
+{
+ "name": "go test",
+ "on": ["push", "pull_request"],
+ "jobs": {
+ "test": {
+ "strategy": {
+ "matrix": {
+ "go-version": ["1.13.x", "1.20.x"],
+ "os": ["ubuntu-latest", "macos-latest", "windows-latest"]
+ }
+ },
+ "runs-on": "${{ matrix.os }}",
+ "env": {"GOPROXY": "direct"},
+ "steps": [{
+ "name": "Install Go",
+ "uses": "actions/setup-go@v3",
+ "with": {"go-version": "${{ matrix.go-version }}"}
+ }, {
+ "name": "Checkout code",
+ "uses": "actions/checkout@v3"
+ }, {
+ "name": "Test",
+ "run": "go test -race ./..."
+ }, {
+ "name": "Test on 32bit",
+ "if": "runner.os == 'Linux'",
+ "run": "GOARCH=386 go test ./..."
+ }]
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fe79e3a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/toml.test
+/toml-test
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..01b5743
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 TOML authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3651cfa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,120 @@
+TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
+reflection interface similar to Go's standard library `json` and `xml` packages.
+
+Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
+
+Documentation: https://godocs.io/github.com/BurntSushi/toml
+
+See the [releases page](https://github.com/BurntSushi/toml/releases) for a
+changelog; this information is also in the git tag annotations (e.g. `git show
+v0.4.0`).
+
+This library requires Go 1.13 or newer; add it to your go.mod with:
+
+ % go get github.com/BurntSushi/toml@latest
+
+It also comes with a TOML validator CLI tool:
+
+ % go install github.com/BurntSushi/toml/cmd/tomlv@latest
+ % tomlv some-toml-file.toml
+
+### Examples
+For the simplest example, consider some TOML file as just a list of keys and
+values:
+
+```toml
+Age = 25
+Cats = [ "Cauchy", "Plato" ]
+Pi = 3.14
+Perfection = [ 6, 28, 496, 8128 ]
+DOB = 1987-07-05T05:45:00Z
+```
+
+Which can be decoded with:
+
+```go
+type Config struct {
+ Age int
+ Cats []string
+ Pi float64
+ Perfection []int
+ DOB time.Time
+}
+
+var conf Config
+_, err := toml.Decode(tomlData, &conf)
+```
+
+You can also use struct tags if your struct field name doesn't map to a TOML key
+value directly:
+
+```toml
+some_key_NAME = "wat"
+```
+
+```go
+type TOML struct {
+ ObscureKey string `toml:"some_key_NAME"`
+}
+```
+
+Beware that like other decoders **only exported fields** are considered when
+encoding and decoding; private fields are silently ignored.
+
+### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
+Here's an example that automatically parses values in a `mail.Address`:
+
+```toml
+contacts = [
+ "Donald Duck <donald@duckburg.com>",
+ "Scrooge McDuck <scrooge@duckburg.com>",
+]
+```
+
+Can be decoded with:
+
+```go
+// Create address type which satisfies the encoding.TextUnmarshaler interface.
+type address struct {
+ *mail.Address
+}
+
+func (a *address) UnmarshalText(text []byte) error {
+ var err error
+ a.Address, err = mail.ParseAddress(string(text))
+ return err
+}
+
+// Decode it.
+func decode() {
+ blob := `
+ contacts = [
+ "Donald Duck <donald@duckburg.com>",
+ "Scrooge McDuck <scrooge@duckburg.com>",
+ ]
+ `
+
+ var contacts struct {
+ Contacts []address
+ }
+
+ _, err := toml.Decode(blob, &contacts)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, c := range contacts.Contacts {
+ fmt.Printf("%#v\n", c.Address)
+ }
+
+ // Output:
+ // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
+ // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
+}
+```
+
+To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
+a similar way.
+
+### More complex usage
+See the [`_example/`](/_example) directory for a more complex example.
diff --git a/_example/example.go b/_example/example.go
new file mode 100644
index 0000000..d2af558
--- /dev/null
+++ b/_example/example.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/BurntSushi/toml"
+)
+
+type (
+ example struct {
+ Title string
+ Desc string
+ Integers []int
+ Floats []float64
+ Times []fmtTime
+ Duration []time.Duration
+ Distros []distro
+ Servers map[string]server
+ Characters map[string][]struct {
+ Name string
+ Rank string
+ }
+ }
+
+ server struct {
+ IP string
+ Hostname string
+ Enabled bool
+ }
+
+ distro struct {
+ Name string
+ Packages string
+ }
+
+ fmtTime struct{ time.Time }
+)
+
+func (t fmtTime) String() string {
+ f := "2006-01-02 15:04:05.999999999"
+ if t.Time.Hour() == 0 {
+ f = "2006-01-02"
+ }
+ if t.Time.Year() == 0 {
+ f = "15:04:05.999999999"
+ }
+ if t.Time.Location() == time.UTC {
+ f += " UTC"
+ } else {
+ f += " -0700"
+ }
+ return t.Time.Format(`"` + f + `"`)
+}
+
+func main() {
+ f := "example.toml"
+ if _, err := os.Stat(f); err != nil {
+ f = "_example/example.toml"
+ }
+
+ var config example
+ meta, err := toml.DecodeFile(f, &config)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ indent := strings.Repeat(" ", 14)
+
+ fmt.Print("Decoded")
+ typ, val := reflect.TypeOf(config), reflect.ValueOf(config)
+ for i := 0; i < typ.NumField(); i++ {
+ indent := indent
+ if i == 0 {
+ indent = strings.Repeat(" ", 7)
+ }
+ fmt.Printf("%s%-11s → %v\n", indent, typ.Field(i).Name, val.Field(i).Interface())
+ }
+
+ fmt.Print("\nKeys")
+ keys := meta.Keys()
+ sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
+ for i, k := range keys {
+ indent := indent
+ if i == 0 {
+ indent = strings.Repeat(" ", 10)
+ }
+ fmt.Printf("%s%-10s %s\n", indent, meta.Type(k...), k)
+ }
+
+ fmt.Print("\nUndecoded")
+ keys = meta.Undecoded()
+ sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
+ for i, k := range keys {
+ indent := indent
+ if i == 0 {
+ indent = strings.Repeat(" ", 5)
+ }
+ fmt.Printf("%s%-10s %s\n", indent, meta.Type(k...), k)
+ }
+}
diff --git a/_example/example.toml b/_example/example.toml
new file mode 100644
index 0000000..b89c504
--- /dev/null
+++ b/_example/example.toml
@@ -0,0 +1,53 @@
+# This is an example TOML document which shows most of its features.
+
+# Simple key/value with a string.
+title = "TOML example \U0001F60A"
+
+desc = """
+An example TOML document. \
+"""
+
+# Array with integers and floats in the various allowed formats.
+integers = [42, 0x42, 0o42, 0b0110]
+floats = [1.42, 1e-02]
+
+# Array with supported datetime formats.
+times = [
+ 2021-11-09T15:16:17+01:00, # datetime with timezone.
+ 2021-11-09T15:16:17Z, # UTC datetime.
+ 2021-11-09T15:16:17, # local datetime.
+ 2021-11-09, # local date.
+ 15:16:17, # local time.
+]
+
+# Durations.
+duration = ["4m49s", "8m03s", "1231h15m55s"]
+
+# Table with inline tables.
+distros = [
+ {name = "Arch Linux", packages = "pacman"},
+ {name = "Void Linux", packages = "xbps"},
+ {name = "Debian", packages = "apt"},
+]
+
+# Create new table; note the "servers" table is created implicitly.
+[servers.alpha]
+ # You can indent as you please, tabs or spaces.
+ ip = '10.0.0.1'
+ hostname = 'server1'
+ enabled = false
+[servers.beta]
+ ip = '10.0.0.2'
+ hostname = 'server2'
+ enabled = true
+
+# Start a new table array; note that the "characters" table is created implicitly.
+[[characters.star-trek]]
+ name = "James Kirk"
+ rank = "Captain"
+[[characters.star-trek]]
+ name = "Spock"
+ rank = "Science officer"
+
+[undecoded] # To show the MetaData.Undecoded() feature.
+ key = "This table intentionally left undecoded"
diff --git a/bench_test.go b/bench_test.go
new file mode 100644
index 0000000..198ce5c
--- /dev/null
+++ b/bench_test.go
@@ -0,0 +1,218 @@
+//go:build go1.16
+// +build go1.16
+
+package toml_test
+
+import (
+ "bytes"
+ "io/fs"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/BurntSushi/toml"
+ tomltest "github.com/BurntSushi/toml/internal/toml-test"
+)
+
+func BenchmarkDecode(b *testing.B) {
+ files := make(map[string][]string)
+ fs.WalkDir(tomltest.EmbeddedTests(), ".", func(path string, d fs.DirEntry, err error) error {
+ if strings.HasPrefix(path, "valid/") && strings.HasSuffix(path, ".toml") {
+ d, _ := fs.ReadFile(tomltest.EmbeddedTests(), path)
+ g := filepath.Dir(path[6:])
+ if g == "." {
+ g = "top"
+ }
+ files[g] = append(files[g], string(d))
+ }
+ return nil
+ })
+
+ type test struct {
+ group string
+ toml []string
+ }
+ tests := make([]test, 0, len(files))
+ for k, v := range files {
+ tests = append(tests, test{group: k, toml: v})
+ }
+ sort.Slice(tests, func(i, j int) bool { return tests[i].group < tests[j].group })
+
+ b.ResetTimer()
+ for _, tt := range tests {
+ b.Run(tt.group, func(b *testing.B) {
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ for _, f := range tt.toml {
+ var val map[string]interface{}
+ toml.Decode(f, &val)
+ }
+ }
+ })
+ }
+
+ b.Run("large-doc", func(b *testing.B) {
+ d, err := os.ReadFile("testdata/ja-JP.toml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ doc := string(d)
+
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ var val map[string]interface{}
+ toml.Decode(doc, &val)
+ }
+ })
+}
+
+func BenchmarkEncode(b *testing.B) {
+ files := make(map[string][]map[string]interface{})
+ fs.WalkDir(tomltest.EmbeddedTests(), ".", func(path string, d fs.DirEntry, err error) error {
+ if strings.HasPrefix(path, "valid/") && strings.HasSuffix(path, ".toml") {
+ d, _ := fs.ReadFile(tomltest.EmbeddedTests(), path)
+ g := filepath.Dir(path[6:])
+ if g == "." {
+ g = "top"
+ }
+
+ // "next" version of TOML.
+ switch path {
+ case "valid/string/escape-esc.toml", "valid/datetime/no-seconds.toml",
+ "valid/string/hex-escape.toml", "valid/inline-table/newline.toml",
+ "valid/key/unicode.toml":
+ return nil
+ }
+
+ var dec map[string]interface{}
+ _, err := toml.Decode(string(d), &dec)
+ if err != nil {
+ b.Fatalf("decode %q: %s", path, err)
+ }
+
+ buf := new(bytes.Buffer)
+ err = toml.NewEncoder(buf).Encode(dec)
+ if err != nil {
+ b.Logf("encode failed for %q (skipping): %s", path, err)
+ return nil
+ }
+
+ files[g] = append(files[g], dec)
+ }
+ return nil
+ })
+
+ type test struct {
+ group string
+ data []map[string]interface{}
+ }
+ tests := make([]test, 0, len(files))
+ for k, v := range files {
+ tests = append(tests, test{group: k, data: v})
+ }
+ sort.Slice(tests, func(i, j int) bool { return tests[i].group < tests[j].group })
+
+ b.ResetTimer()
+ for _, tt := range tests {
+ b.Run(tt.group, func(b *testing.B) {
+ buf := new(bytes.Buffer)
+ buf.Grow(1024 * 64)
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ for _, f := range tt.data {
+ toml.NewEncoder(buf).Encode(f)
+ }
+ }
+ })
+ }
+}
+
+func BenchmarkExample(b *testing.B) {
+ d, err := ioutil.ReadFile("_example/example.toml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ t := string(d)
+
+ var decoded example
+ _, err = toml.Decode(t, &decoded)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ buf := new(bytes.Buffer)
+ err = toml.NewEncoder(buf).Encode(decoded)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.Run("decode", func(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ var c example
+ toml.Decode(t, &c)
+ }
+ })
+
+ b.Run("encode", func(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ buf.Reset()
+ toml.NewEncoder(buf).Encode(decoded)
+ }
+ })
+}
+
+// Copy from _example/example.go
+type (
+ example struct {
+ Title string
+ Integers []int
+ Times []fmtTime
+ Duration []duration
+ Distros []distro
+ Servers map[string]server
+ Characters map[string][]struct {
+ Name string
+ Rank string
+ }
+ }
+
+ server struct {
+ IP string
+ Hostname string
+ Enabled bool
+ }
+
+ distro struct {
+ Name string
+ Packages string
+ }
+
+ duration struct{ time.Duration }
+ fmtTime struct{ time.Time }
+)
+
+func (d *duration) UnmarshalText(text []byte) (err error) {
+ d.Duration, err = time.ParseDuration(string(text))
+ return err
+}
+
+func (t fmtTime) String() string {
+ f := "2006-01-02 15:04:05.999999999"
+ if t.Time.Hour() == 0 {
+ f = "2006-01-02"
+ }
+ if t.Time.Year() == 0 {
+ f = "15:04:05.999999999"
+ }
+ if t.Time.Location() == time.UTC {
+ f += " UTC"
+ } else {
+ f += " -0700"
+ }
+ return t.Time.Format(`"` + f + `"`)
+}
diff --git a/cmd/toml-test-decoder/COPYING b/cmd/toml-test-decoder/COPYING
new file mode 100644
index 0000000..01b5743
--- /dev/null
+++ b/cmd/toml-test-decoder/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 TOML authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/cmd/toml-test-decoder/README.md b/cmd/toml-test-decoder/README.md
new file mode 100644
index 0000000..75b501c
--- /dev/null
+++ b/cmd/toml-test-decoder/README.md
@@ -0,0 +1,6 @@
+# Implements the TOML test suite interface
+
+This is an implementation of the interface expected by
+[toml-test](https://github.com/BurntSushi/toml-test) for my
+[toml parser written in Go](https://github.com/BurntSushi/toml).
+In particular, it maps TOML data on `stdin` to a JSON format on `stdout`.
diff --git a/cmd/toml-test-decoder/main.go b/cmd/toml-test-decoder/main.go
new file mode 100644
index 0000000..0823bbf
--- /dev/null
+++ b/cmd/toml-test-decoder/main.go
@@ -0,0 +1,43 @@
+// Command toml-test-decoder satisfies the toml-test interface for testing TOML
+// decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "log"
+ "os"
+ "path"
+
+ "github.com/BurntSushi/toml"
+ "github.com/BurntSushi/toml/internal/tag"
+)
+
+func init() {
+ log.SetFlags(0)
+ flag.Usage = usage
+ flag.Parse()
+}
+
+func usage() {
+ log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
+ flag.PrintDefaults()
+ os.Exit(1)
+}
+
+func main() {
+ if flag.NArg() != 0 {
+ flag.Usage()
+ }
+
+ var decoded interface{}
+ if _, err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
+ log.Fatalf("Error decoding TOML: %s", err)
+ }
+
+ j := json.NewEncoder(os.Stdout)
+ j.SetIndent("", " ")
+ if err := j.Encode(tag.Add("", decoded)); err != nil {
+ log.Fatalf("Error encoding JSON: %s", err)
+ }
+}
diff --git a/cmd/toml-test-encoder/COPYING b/cmd/toml-test-encoder/COPYING
new file mode 100644
index 0000000..01b5743
--- /dev/null
+++ b/cmd/toml-test-encoder/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 TOML authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/cmd/toml-test-encoder/README.md b/cmd/toml-test-encoder/README.md
new file mode 100644
index 0000000..a106532
--- /dev/null
+++ b/cmd/toml-test-encoder/README.md
@@ -0,0 +1,6 @@
+# Implements the TOML test suite interface for TOML encoders
+
+This is an implementation of the interface expected by
+[toml-test](https://github.com/BurntSushi/toml-test) for the
+[TOML encoder](https://github.com/BurntSushi/toml).
+In particular, it maps JSON data on `stdin` to a TOML format on `stdout`.
diff --git a/cmd/toml-test-encoder/main.go b/cmd/toml-test-encoder/main.go
new file mode 100644
index 0000000..b5e47ab
--- /dev/null
+++ b/cmd/toml-test-encoder/main.go
@@ -0,0 +1,46 @@
+// Command toml-test-encoder satisfies the toml-test interface for testing TOML
+// encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "log"
+ "os"
+ "path"
+
+ "github.com/BurntSushi/toml"
+ "github.com/BurntSushi/toml/internal/tag"
+)
+
+func init() {
+ log.SetFlags(0)
+ flag.Usage = usage
+ flag.Parse()
+}
+
+func usage() {
+ log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
+ flag.PrintDefaults()
+ os.Exit(1)
+}
+
+func main() {
+ if flag.NArg() != 0 {
+ flag.Usage()
+ }
+
+ var tmp interface{}
+ if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
+ log.Fatalf("Error decoding JSON: %s", err)
+ }
+
+ rm, err := tag.Remove(tmp)
+ if err != nil {
+ log.Fatalf("Error decoding JSON: %s", err)
+ }
+
+ if err := toml.NewEncoder(os.Stdout).Encode(rm); err != nil {
+ log.Fatalf("Error encoding TOML: %s", err)
+ }
+}
diff --git a/cmd/tomlv/COPYING b/cmd/tomlv/COPYING
new file mode 100644
index 0000000..01b5743
--- /dev/null
+++ b/cmd/tomlv/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 TOML authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/cmd/tomlv/README.md b/cmd/tomlv/README.md
new file mode 100644
index 0000000..53c6fb0
--- /dev/null
+++ b/cmd/tomlv/README.md
@@ -0,0 +1,14 @@
+# TOML Validator
+
+If Go is installed, it's simple to try it out:
+
+ $ go install github.com/BurntSushi/toml/cmd/tomlv@master
+ $ tomlv some-toml-file.toml
+
+You can see the types of every key in a TOML file with:
+
+ $ tomlv -types some-toml-file.toml
+
+At the moment, only one error message is reported at a time. Error messages
+include line numbers. No output means that the files given are valid TOML, or
+there is a bug in `tomlv`.
diff --git a/cmd/tomlv/main.go b/cmd/tomlv/main.go
new file mode 100644
index 0000000..0eeb3ce
--- /dev/null
+++ b/cmd/tomlv/main.go
@@ -0,0 +1,56 @@
+// Command tomlv validates TOML documents and prints each key's type.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/BurntSushi/toml"
+)
+
+var (
+ flagTypes = false
+)
+
+func init() {
+ log.SetFlags(0)
+ flag.BoolVar(&flagTypes, "types", flagTypes, "Show the types for every key.")
+ flag.Usage = usage
+ flag.Parse()
+}
+
+func usage() {
+ log.Printf("Usage: %s toml-file [ toml-file ... ]\n", path.Base(os.Args[0]))
+ flag.PrintDefaults()
+ os.Exit(1)
+}
+
+func main() {
+ if flag.NArg() < 1 {
+ flag.Usage()
+ }
+ for _, f := range flag.Args() {
+ var tmp interface{}
+ md, err := toml.DecodeFile(f, &tmp)
+ if err != nil {
+ log.Fatalf("Error in '%s': %s", f, err)
+ }
+ if flagTypes {
+ printTypes(md)
+ }
+ }
+}
+
+func printTypes(md toml.MetaData) {
+ tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+ for _, key := range md.Keys() {
+ fmt.Fprintf(tabw, "%s%s\t%s\n",
+ strings.Repeat(" ", len(key)-1), key, md.Type(key...))
+ }
+ tabw.Flush()
+}
diff --git a/decode.go b/decode.go
new file mode 100644
index 0000000..4d38f3b
--- /dev/null
+++ b/decode.go
@@ -0,0 +1,602 @@
+package toml
+
+import (
+ "bytes"
+ "encoding"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Unmarshaler is the interface implemented by objects that can unmarshal a
+// TOML description of themselves.
+type Unmarshaler interface {
+ UnmarshalTOML(interface{}) error
+}
+
+// Unmarshal decodes the contents of data in TOML format into a pointer v.
+//
+// See [Decoder] for a description of the decoding process.
+func Unmarshal(data []byte, v interface{}) error {
+ _, err := NewDecoder(bytes.NewReader(data)).Decode(v)
+ return err
+}
+
+// Decode the TOML data in to the pointer v.
+//
+// See [Decoder] for a description of the decoding process.
+func Decode(data string, v interface{}) (MetaData, error) {
+ return NewDecoder(strings.NewReader(data)).Decode(v)
+}
+
+// DecodeFile reads the contents of a file and decodes it with [Decode].
+func DecodeFile(path string, v interface{}) (MetaData, error) {
+ fp, err := os.Open(path)
+ if err != nil {
+ return MetaData{}, err
+ }
+ defer fp.Close()
+ return NewDecoder(fp).Decode(v)
+}
+
+// Primitive is a TOML value that hasn't been decoded into a Go value.
+//
+// This type can be used for any value, which will cause decoding to be delayed.
+// You can use [PrimitiveDecode] to "manually" decode these values.
+//
+// NOTE: The underlying representation of a `Primitive` value is subject to
+// change. Do not rely on it.
+//
+// NOTE: Primitive values are still parsed, so using them will only avoid the
+// overhead of reflection. They can be useful when you don't know the exact type
+// of TOML data until runtime.
+type Primitive struct {
+ undecoded interface{}
+ context Key
+}
+
+// The significand precision for float32 and float64 is 24 and 53 bits; this is
+// the range a natural number can be stored in a float without loss of data.
+const (
+ maxSafeFloat32Int = 16777215 // 2^24-1
+ maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
+)
+
+// Decoder decodes TOML data.
+//
+// TOML tables correspond to Go structs or maps; they can be used
+// interchangeably, but structs offer better type safety.
+//
+// TOML table arrays correspond to either a slice of structs or a slice of maps.
+//
+// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
+// local timezone.
+//
+// [time.Duration] types are treated as nanoseconds if the TOML value is an
+// integer, or they're parsed with time.ParseDuration() if they're strings.
+//
+// All other TOML types (float, string, int, bool and array) correspond to the
+// obvious Go types.
+//
+// An exception to the above rules is if a type implements the TextUnmarshaler
+// interface, in which case any primitive TOML value (floats, strings, integers,
+// booleans, datetimes) will be converted to a []byte and given to the value's
+// UnmarshalText method. See the Unmarshaler example for a demonstration with
+// email addresses.
+//
+// # Key mapping
+//
+// TOML keys can map to either keys in a Go map or field names in a Go struct.
+// The special `toml` struct tag can be used to map TOML keys to struct fields
+// that don't match the key name exactly (see the example). A case insensitive
+// match to struct names will be tried if an exact match can't be found.
+//
+// The mapping between TOML values and Go values is loose. That is, there may
+// exist TOML values that cannot be placed into your representation, and there
+// may be parts of your representation that do not correspond to TOML values.
+// This loose mapping can be made stricter by using the IsDefined and/or
+// Undecoded methods on the MetaData returned.
+//
+// This decoder does not handle cyclic types. Decode will not terminate if a
+// cyclic type is passed.
+type Decoder struct {
+ r io.Reader
+}
+
+// NewDecoder creates a new Decoder.
+func NewDecoder(r io.Reader) *Decoder {
+ return &Decoder{r: r}
+}
+
+var (
+ unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
+ unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
+ primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
+)
+
+// Decode TOML data in to the pointer `v`.
+func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr {
+ s := "%q"
+ if reflect.TypeOf(v) == nil {
+ s = "%v"
+ }
+
+ return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
+ }
+ if rv.IsNil() {
+ return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
+ }
+
+ // Check if this is a supported type: struct, map, interface{}, or something
+ // that implements UnmarshalTOML or UnmarshalText.
+ rv = indirect(rv)
+ rt := rv.Type()
+ if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
+ !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
+ !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
+ return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
+ }
+
+ // TODO: parser should read from io.Reader? Or at the very least, make it
+ // read from []byte rather than string
+ data, err := ioutil.ReadAll(dec.r)
+ if err != nil {
+ return MetaData{}, err
+ }
+
+ p, err := parse(string(data))
+ if err != nil {
+ return MetaData{}, err
+ }
+
+ md := MetaData{
+ mapping: p.mapping,
+ keyInfo: p.keyInfo,
+ keys: p.ordered,
+ decoded: make(map[string]struct{}, len(p.ordered)),
+ context: nil,
+ data: data,
+ }
+ return md, md.unify(p.mapping, rv)
+}
+
+// PrimitiveDecode is just like the other Decode* functions, except it decodes a
+// TOML value that has already been parsed. Valid primitive values can *only* be
+// obtained from values filled by the decoder functions, including this method.
+// (i.e., v may contain more [Primitive] values.)
+//
+// Meta data for primitive values is included in the meta data returned by the
+// Decode* functions with one exception: keys returned by the Undecoded method
+// will only reflect keys that were decoded. Namely, any keys hidden behind a
+// Primitive will be considered undecoded. Executing this method will update the
+// undecoded keys in the meta data. (See the example.)
+func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
+ md.context = primValue.context
+ defer func() { md.context = nil }()
+ return md.unify(primValue.undecoded, rvalue(v))
+}
+
+// unify performs a sort of type unification based on the structure of `rv`,
+// which is the client representation.
+//
+// Any type mismatch produces an error. Finding a type that we don't know
+// how to handle produces an unsupported type error.
+func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
+ // Special case. Look for a `Primitive` value.
+ // TODO: #76 would make this superfluous after implemented.
+ if rv.Type() == primitiveType {
+ // Save the undecoded data and the key context into the primitive
+ // value.
+ context := make(Key, len(md.context))
+ copy(context, md.context)
+ rv.Set(reflect.ValueOf(Primitive{
+ undecoded: data,
+ context: context,
+ }))
+ return nil
+ }
+
+ rvi := rv.Interface()
+ if v, ok := rvi.(Unmarshaler); ok {
+ return v.UnmarshalTOML(data)
+ }
+ if v, ok := rvi.(encoding.TextUnmarshaler); ok {
+ return md.unifyText(data, v)
+ }
+
+ // TODO:
+ // The behavior here is incorrect whenever a Go type satisfies the
+ // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
+ // array. In particular, the unmarshaler should only be applied to primitive
+ // TOML values. But at this point, it will be applied to all kinds of values
+ // and produce an incorrect error whenever those values are hashes or arrays
+ // (including arrays of tables).
+
+ k := rv.Kind()
+
+ if k >= reflect.Int && k <= reflect.Uint64 {
+ return md.unifyInt(data, rv)
+ }
+ switch k {
+ case reflect.Ptr:
+ elem := reflect.New(rv.Type().Elem())
+ err := md.unify(data, reflect.Indirect(elem))
+ if err != nil {
+ return err
+ }
+ rv.Set(elem)
+ return nil
+ case reflect.Struct:
+ return md.unifyStruct(data, rv)
+ case reflect.Map:
+ return md.unifyMap(data, rv)
+ case reflect.Array:
+ return md.unifyArray(data, rv)
+ case reflect.Slice:
+ return md.unifySlice(data, rv)
+ case reflect.String:
+ return md.unifyString(data, rv)
+ case reflect.Bool:
+ return md.unifyBool(data, rv)
+ case reflect.Interface:
+ if rv.NumMethod() > 0 { /// Only empty interfaces are supported.
+ return md.e("unsupported type %s", rv.Type())
+ }
+ return md.unifyAnything(data, rv)
+ case reflect.Float32, reflect.Float64:
+ return md.unifyFloat64(data, rv)
+ }
+ return md.e("unsupported type %s", rv.Kind())
+}
+
+func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
+ tmap, ok := mapping.(map[string]interface{})
+ if !ok {
+ if mapping == nil {
+ return nil
+ }
+ return md.e("type mismatch for %s: expected table but found %T",
+ rv.Type().String(), mapping)
+ }
+
+ for key, datum := range tmap {
+ var f *field
+ fields := cachedTypeFields(rv.Type())
+ for i := range fields {
+ ff := &fields[i]
+ if ff.name == key {
+ f = ff
+ break
+ }
+ if f == nil && strings.EqualFold(ff.name, key) {
+ f = ff
+ }
+ }
+ if f != nil {
+ subv := rv
+ for _, i := range f.index {
+ subv = indirect(subv.Field(i))
+ }
+
+ if isUnifiable(subv) {
+ md.decoded[md.context.add(key).String()] = struct{}{}
+ md.context = append(md.context, key)
+
+ err := md.unify(datum, subv)
+ if err != nil {
+ return err
+ }
+ md.context = md.context[0 : len(md.context)-1]
+ } else if f.name != "" {
+ return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
+ }
+ }
+ }
+ return nil
+}
+
+func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
+ keyType := rv.Type().Key().Kind()
+ if keyType != reflect.String && keyType != reflect.Interface {
+ return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
+ keyType, rv.Type())
+ }
+
+ tmap, ok := mapping.(map[string]interface{})
+ if !ok {
+ if tmap == nil {
+ return nil
+ }
+ return md.badtype("map", mapping)
+ }
+ if rv.IsNil() {
+ rv.Set(reflect.MakeMap(rv.Type()))
+ }
+ for k, v := range tmap {
+ md.decoded[md.context.add(k).String()] = struct{}{}
+ md.context = append(md.context, k)
+
+ rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
+
+ err := md.unify(v, indirect(rvval))
+ if err != nil {
+ return err
+ }
+ md.context = md.context[0 : len(md.context)-1]
+
+ rvkey := indirect(reflect.New(rv.Type().Key()))
+
+ switch keyType {
+ case reflect.Interface:
+ rvkey.Set(reflect.ValueOf(k))
+ case reflect.String:
+ rvkey.SetString(k)
+ }
+
+ rv.SetMapIndex(rvkey, rvval)
+ }
+ return nil
+}
+
+func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
+ datav := reflect.ValueOf(data)
+ if datav.Kind() != reflect.Slice {
+ if !datav.IsValid() {
+ return nil
+ }
+ return md.badtype("slice", data)
+ }
+ if l := datav.Len(); l != rv.Len() {
+ return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
+ }
+ return md.unifySliceArray(datav, rv)
+}
+
+func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
+ datav := reflect.ValueOf(data)
+ if datav.Kind() != reflect.Slice {
+ if !datav.IsValid() {
+ return nil
+ }
+ return md.badtype("slice", data)
+ }
+ n := datav.Len()
+ if rv.IsNil() || rv.Cap() < n {
+ rv.Set(reflect.MakeSlice(rv.Type(), n, n))
+ }
+ rv.SetLen(n)
+ return md.unifySliceArray(datav, rv)
+}
+
+func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
+ l := data.Len()
+ for i := 0; i < l; i++ {
+ err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i)))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
+ _, ok := rv.Interface().(json.Number)
+ if ok {
+ if i, ok := data.(int64); ok {
+ rv.SetString(strconv.FormatInt(i, 10))
+ } else if f, ok := data.(float64); ok {
+ rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
+ } else {
+ return md.badtype("string", data)
+ }
+ return nil
+ }
+
+ if s, ok := data.(string); ok {
+ rv.SetString(s)
+ return nil
+ }
+ return md.badtype("string", data)
+}
+
+func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
+ rvk := rv.Kind()
+
+ if num, ok := data.(float64); ok {
+ switch rvk {
+ case reflect.Float32:
+ if num < -math.MaxFloat32 || num > math.MaxFloat32 {
+ return md.parseErr(errParseRange{i: num, size: rvk.String()})
+ }
+ fallthrough
+ case reflect.Float64:
+ rv.SetFloat(num)
+ default:
+ panic("bug")
+ }
+ return nil
+ }
+
+ if num, ok := data.(int64); ok {
+ if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
+ (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
+ return md.parseErr(errParseRange{i: num, size: rvk.String()})
+ }
+ rv.SetFloat(float64(num))
+ return nil
+ }
+
+ return md.badtype("float", data)
+}
+
+func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
+ _, ok := rv.Interface().(time.Duration)
+ if ok {
+ // Parse as string duration, and fall back to regular integer parsing
+ // (as nanosecond) if this is not a string.
+ if s, ok := data.(string); ok {
+ dur, err := time.ParseDuration(s)
+ if err != nil {
+ return md.parseErr(errParseDuration{s})
+ }
+ rv.SetInt(int64(dur))
+ return nil
+ }
+ }
+
+ num, ok := data.(int64)
+ if !ok {
+ return md.badtype("integer", data)
+ }
+
+ rvk := rv.Kind()
+ switch {
+ case rvk >= reflect.Int && rvk <= reflect.Int64:
+ if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
+ (rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
+ (rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
+ return md.parseErr(errParseRange{i: num, size: rvk.String()})
+ }
+ rv.SetInt(num)
+ case rvk >= reflect.Uint && rvk <= reflect.Uint64:
+ unum := uint64(num)
+ if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
+ rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
+ rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
+ return md.parseErr(errParseRange{i: num, size: rvk.String()})
+ }
+ rv.SetUint(unum)
+ default:
+ panic("unreachable")
+ }
+ return nil
+}
+
+func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
+ if b, ok := data.(bool); ok {
+ rv.SetBool(b)
+ return nil
+ }
+ return md.badtype("boolean", data)
+}
+
+func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
+ rv.Set(reflect.ValueOf(data))
+ return nil
+}
+
+func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error {
+ var s string
+ switch sdata := data.(type) {
+ case Marshaler:
+ text, err := sdata.MarshalTOML()
+ if err != nil {
+ return err
+ }
+ s = string(text)
+ case encoding.TextMarshaler:
+ text, err := sdata.MarshalText()
+ if err != nil {
+ return err
+ }
+ s = string(text)
+ case fmt.Stringer:
+ s = sdata.String()
+ case string:
+ s = sdata
+ case bool:
+ s = fmt.Sprintf("%v", sdata)
+ case int64:
+ s = fmt.Sprintf("%d", sdata)
+ case float64:
+ s = fmt.Sprintf("%f", sdata)
+ default:
+ return md.badtype("primitive (string-like)", data)
+ }
+ if err := v.UnmarshalText([]byte(s)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (md *MetaData) badtype(dst string, data interface{}) error {
+ return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst)
+}
+
+func (md *MetaData) parseErr(err error) error {
+ k := md.context.String()
+ return ParseError{
+ LastKey: k,
+ Position: md.keyInfo[k].pos,
+ Line: md.keyInfo[k].pos.Line,
+ err: err,
+ input: string(md.data),
+ }
+}
+
+func (md *MetaData) e(format string, args ...interface{}) error {
+ f := "toml: "
+ if len(md.context) > 0 {
+ f = fmt.Sprintf("toml: (last key %q): ", md.context)
+ p := md.keyInfo[md.context.String()].pos
+ if p.Line > 0 {
+ f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
+ }
+ }
+ return fmt.Errorf(f+format, args...)
+}
+
+// rvalue returns a reflect.Value of `v`. All pointers are resolved.
+func rvalue(v interface{}) reflect.Value {
+ return indirect(reflect.ValueOf(v))
+}
+
+// indirect returns the value pointed to by a pointer.
+//
+// Pointers are followed until the value is not a pointer. New values are
+// allocated for each nil pointer.
+//
+// An exception to this rule is if the value satisfies an interface of interest
+// to us (like encoding.TextUnmarshaler).
+func indirect(v reflect.Value) reflect.Value {
+ if v.Kind() != reflect.Ptr {
+ if v.CanSet() {
+ pv := v.Addr()
+ pvi := pv.Interface()
+ if _, ok := pvi.(encoding.TextUnmarshaler); ok {
+ return pv
+ }
+ if _, ok := pvi.(Unmarshaler); ok {
+ return pv
+ }
+ }
+ return v
+ }
+ if v.IsNil() {
+ v.Set(reflect.New(v.Type().Elem()))
+ }
+ return indirect(reflect.Indirect(v))
+}
+
+func isUnifiable(rv reflect.Value) bool {
+ if rv.CanSet() {
+ return true
+ }
+ rvi := rv.Interface()
+ if _, ok := rvi.(encoding.TextUnmarshaler); ok {
+ return true
+ }
+ if _, ok := rvi.(Unmarshaler); ok {
+ return true
+ }
+ return false
+}
diff --git a/decode_go116.go b/decode_go116.go
new file mode 100644
index 0000000..086d0b6
--- /dev/null
+++ b/decode_go116.go
@@ -0,0 +1,19 @@
+//go:build go1.16
+// +build go1.16
+
+package toml
+
+import (
+ "io/fs"
+)
+
+// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
+// [Decode].
+func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
+ fp, err := fsys.Open(path)
+ if err != nil {
+ return MetaData{}, err
+ }
+ defer fp.Close()
+ return NewDecoder(fp).Decode(v)
+}
diff --git a/decode_go116_test.go b/decode_go116_test.go
new file mode 100644
index 0000000..83722f2
--- /dev/null
+++ b/decode_go116_test.go
@@ -0,0 +1,29 @@
+//go:build go1.16
+// +build go1.16
+
+package toml
+
+import (
+ "fmt"
+ "testing"
+ "testing/fstest"
+)
+
+func TestDecodeFS(t *testing.T) {
+ fsys := fstest.MapFS{
+ "test.toml": &fstest.MapFile{
+ Data: []byte("a = 42"),
+ },
+ }
+
+ var i struct{ A int }
+ meta, err := DecodeFS(fsys, "test.toml", &i)
+ if err != nil {
+ t.Fatal(err)
+ }
+ have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+ want := "{42} [a] Integer"
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
diff --git a/decode_test.go b/decode_test.go
new file mode 100644
index 0000000..6f08d3a
--- /dev/null
+++ b/decode_test.go
@@ -0,0 +1,1238 @@
+package toml
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+func WithTomlNext(f func()) {
+ os.Setenv("BURNTSUSHI_TOML_110", "")
+ defer func() { os.Unsetenv("BURNTSUSHI_TOML_110") }()
+ f()
+}
+
+func TestDecodeReader(t *testing.T) {
+ var i struct{ A int }
+ meta, err := DecodeReader(strings.NewReader("a = 42"), &i)
+ if err != nil {
+ t.Fatal(err)
+ }
+ have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+ want := "{42} [a] Integer"
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+func TestDecodeFile(t *testing.T) {
+ tmp, err := ioutil.TempFile("", "toml-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+ if _, err := tmp.WriteString("a = 42"); err != nil {
+ t.Fatal(err)
+ }
+ if err := tmp.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ var i struct{ A int }
+ meta, err := DecodeFile(tmp.Name(), &i)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+ want := "{42} [a] Integer"
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+func TestDecodeBOM(t *testing.T) {
+ for _, tt := range [][]byte{
+ []byte("\xff\xfea = \"b\""),
+ []byte("\xfe\xffa = \"b\""),
+ []byte("\xef\xbb\xbfa = \"b\""),
+ } {
+ t.Run("", func(t *testing.T) {
+ var s struct{ A string }
+ _, err := Decode(string(tt), &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if s.A != "b" {
+ t.Errorf(`s.A is not "b" but %q`, s.A)
+ }
+ })
+ }
+}
+
+func TestDecodeEmbedded(t *testing.T) {
+ type Dog struct{ Name string }
+ type Age int
+ type cat struct{ Name string }
+
+ for _, test := range []struct {
+ label string
+ input string
+ decodeInto interface{}
+ wantDecoded interface{}
+ }{
+ {
+ label: "embedded struct",
+ input: `Name = "milton"`,
+ decodeInto: &struct{ Dog }{},
+ wantDecoded: &struct{ Dog }{Dog{"milton"}},
+ },
+ {
+ label: "embedded non-nil pointer to struct",
+ input: `Name = "milton"`,
+ decodeInto: &struct{ *Dog }{},
+ wantDecoded: &struct{ *Dog }{&Dog{"milton"}},
+ },
+ {
+ label: "embedded nil pointer to struct",
+ input: ``,
+ decodeInto: &struct{ *Dog }{},
+ wantDecoded: &struct{ *Dog }{nil},
+ },
+ {
+ label: "unexported embedded struct",
+ input: `Name = "socks"`,
+ decodeInto: &struct{ cat }{},
+ wantDecoded: &struct{ cat }{cat{"socks"}},
+ },
+ {
+ label: "embedded int",
+ input: `Age = -5`,
+ decodeInto: &struct{ Age }{},
+ wantDecoded: &struct{ Age }{-5},
+ },
+ } {
+ _, err := Decode(test.input, test.decodeInto)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(test.wantDecoded, test.decodeInto) {
+ t.Errorf("%s: want decoded == %+v, got %+v",
+ test.label, test.wantDecoded, test.decodeInto)
+ }
+ }
+}
+
+func TestDecodeErrors(t *testing.T) {
+ tests := []struct {
+ s interface{}
+ toml string
+ wantErr string
+ }{
+ {
+ &struct{ V int8 }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): 999 is out of range for int8`,
+ },
+ {
+ &struct{ V float32 }{},
+ `V = 999999999999999`,
+ `toml: line 1 (last key "V"): 999999999999999 is out of range for float32`,
+ },
+ {
+ &struct{ V string }{},
+ `V = 5`,
+ `toml: line 1 (last key "V"): incompatible types: TOML value has type int64; destination has type string`,
+ },
+ {
+ &struct{ V interface{ ASD() } }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): unsupported type interface { ASD() }`,
+ },
+ {
+ &struct{ V struct{ V int } }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): type mismatch for struct { V int }: expected table but found int64`,
+ },
+ {
+ &struct{ V [1]int }{},
+ `V = [1,2,3]`,
+ `toml: line 1 (last key "V"): expected array length 1; got TOML array of length 3`,
+ },
+ {
+ &struct{ V struct{ N int8 } }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): 999 is out of range for int8`,
+ },
+ {
+ &struct{ V struct{ N float32 } }{},
+ `V.N = 999999999999999`,
+ `toml: line 1 (last key "V.N"): 999999999999999 is out of range for float32`,
+ },
+ {
+ &struct{ V struct{ N string } }{},
+ `V.N = 5`,
+ `toml: line 1 (last key "V.N"): incompatible types: TOML value has type int64; destination has type string`,
+ },
+ {
+ &struct {
+ V struct{ N interface{ ASD() } }
+ }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): unsupported type interface { ASD() }`,
+ },
+ {
+ &struct{ V struct{ N struct{ V int } } }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): type mismatch for struct { V int }: expected table but found int64`,
+ },
+ {
+ &struct{ V struct{ N [1]int } }{},
+ `V.N = [1,2,3]`,
+ `toml: line 1 (last key "V.N"): expected array length 1; got TOML array of length 3`,
+ },
+ }
+
+ for _, tt := range tests {
+ _, err := Decode(tt.toml, tt.s)
+ if err == nil {
+ t.Fatal("err is nil")
+ }
+ if err.Error() != tt.wantErr {
+ t.Errorf("\nhave: %q\nwant: %q", err, tt.wantErr)
+ }
+ }
+}
+
+func TestDecodeIgnoreFields(t *testing.T) {
+ const input = `
+Number = 123
+- = 234
+`
+ var s struct {
+ Number int `toml:"-"`
+ }
+ if _, err := Decode(input, &s); err != nil {
+ t.Fatal(err)
+ }
+ if s.Number != 0 {
+ t.Errorf("got: %d; want 0", s.Number)
+ }
+}
+
+func TestDecodeTableArrays(t *testing.T) {
+ var tomlTableArrays = `
+[[albums]]
+name = "Born to Run"
+
+ [[albums.songs]]
+ name = "Jungleland"
+
+ [[albums.songs]]
+ name = "Meeting Across the River"
+
+[[albums]]
+name = "Born in the USA"
+
+ [[albums.songs]]
+ name = "Glory Days"
+
+ [[albums.songs]]
+ name = "Dancing in the Dark"
+`
+
+ type Song struct {
+ Name string
+ }
+
+ type Album struct {
+ Name string
+ Songs []Song
+ }
+
+ type Music struct {
+ Albums []Album
+ }
+
+ expected := Music{[]Album{
+ {"Born to Run", []Song{{"Jungleland"}, {"Meeting Across the River"}}},
+ {"Born in the USA", []Song{{"Glory Days"}, {"Dancing in the Dark"}}},
+ }}
+ var got Music
+ if _, err := Decode(tomlTableArrays, &got); err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(expected, got) {
+ t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
+ }
+}
+
+func TestDecodePointers(t *testing.T) {
+ type Object struct {
+ Type string
+ Description string
+ }
+
+ type Dict struct {
+ NamedObject map[string]*Object
+ BaseObject *Object
+ Strptr *string
+ Strptrs []*string
+ }
+ s1, s2, s3 := "blah", "abc", "def"
+ expected := &Dict{
+ Strptr: &s1,
+ Strptrs: []*string{&s2, &s3},
+ NamedObject: map[string]*Object{
+ "foo": {"FOO", "fooooo!!!"},
+ "bar": {"BAR", "ba-ba-ba-ba-barrrr!!!"},
+ },
+ BaseObject: &Object{"BASE", "da base"},
+ }
+
+ ex1 := `
+Strptr = "blah"
+Strptrs = ["abc", "def"]
+
+[NamedObject.foo]
+Type = "FOO"
+Description = "fooooo!!!"
+
+[NamedObject.bar]
+Type = "BAR"
+Description = "ba-ba-ba-ba-barrrr!!!"
+
+[BaseObject]
+Type = "BASE"
+Description = "da base"
+`
+ dict := new(Dict)
+ _, err := Decode(ex1, dict)
+ if err != nil {
+ t.Errorf("Decode error: %v", err)
+ }
+ if !reflect.DeepEqual(expected, dict) {
+ t.Fatalf("\n%#v\n!=\n%#v\n", expected, dict)
+ }
+}
+
+func TestDecodeBadDatetime(t *testing.T) {
+ var x struct{ T time.Time }
+ for _, s := range []string{"123", "1230"} {
+ input := "T = " + s
+ if _, err := Decode(input, &x); err == nil {
+ t.Errorf("Expected invalid DateTime error for %q", s)
+ }
+ }
+}
+
+type sphere struct {
+ Center [3]float64
+ Radius float64
+}
+
+func TestDecodeArrayWrongSize(t *testing.T) {
+ var s1 sphere
+ if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil {
+ t.Fatal("Expected array type mismatch error")
+ }
+}
+
+func TestDecodeIntOverflow(t *testing.T) {
+ type table struct {
+ Value int8
+ }
+ var tab table
+ if _, err := Decode(`value = 500`, &tab); err == nil {
+ t.Fatal("Expected integer out-of-bounds error.")
+ }
+}
+
+func TestDecodeFloatOverflow(t *testing.T) {
+ tests := []struct {
+ value string
+ overflow bool
+ }{
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat64), true},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat64), true},
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat32*1.1), true},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat32*1.1), true},
+ {fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int+1), true},
+ {fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int-1), true},
+ {fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int+1), true},
+ {fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int-1), true},
+
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat32), false},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat32), false},
+ {fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int), false},
+ {fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int), false},
+ {fmt.Sprintf(`F64 = %f`, math.MaxFloat64), false},
+ {fmt.Sprintf(`F64 = %f`, -math.MaxFloat64), false},
+ {fmt.Sprintf(`F64 = %f`, math.MaxFloat32), false},
+ {fmt.Sprintf(`F64 = %f`, -math.MaxFloat32), false},
+ {fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int), false},
+ {fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int), false},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var tab struct {
+ F32 float32
+ F64 float64
+ }
+ _, err := Decode(tt.value, &tab)
+
+ if tt.overflow && err == nil {
+ t.Fatal("expected error, but err is nil")
+ }
+ if (tt.overflow && !errorContains(err, "out of range")) || (!tt.overflow && err != nil) {
+ t.Fatalf("unexpected error:\n%v", err)
+ }
+ })
+ }
+}
+
+func TestDecodeSizedInts(t *testing.T) {
+ type table struct {
+ U8 uint8
+ U16 uint16
+ U32 uint32
+ U64 uint64
+ U uint
+ I8 int8
+ I16 int16
+ I32 int32
+ I64 int64
+ I int
+ }
+ answer := table{1, 1, 1, 1, 1, -1, -1, -1, -1, -1}
+ toml := `
+ u8 = 1
+ u16 = 1
+ u32 = 1
+ u64 = 1
+ u = 1
+ i8 = -1
+ i16 = -1
+ i32 = -1
+ i64 = -1
+ i = -1
+ `
+ var tab table
+ if _, err := Decode(toml, &tab); err != nil {
+ t.Fatal(err.Error())
+ }
+ if answer != tab {
+ t.Fatalf("Expected %#v but got %#v", answer, tab)
+ }
+}
+
+type NopUnmarshalTOML int
+
+func (n *NopUnmarshalTOML) UnmarshalTOML(p interface{}) error {
+ *n = 42
+ return nil
+}
+
+func TestDecodeTypes(t *testing.T) {
+ type (
+ mystr string
+ myiface interface{}
+ )
+
+ for _, tt := range []struct {
+ v interface{}
+ want string
+ wantErr string
+ }{
+ {new(map[string]bool), "&map[F:true]", ""},
+ {new(map[mystr]bool), "&map[F:true]", ""},
+ {new(NopUnmarshalTOML), "42", ""},
+ {new(map[interface{}]bool), "&map[F:true]", ""},
+ {new(map[myiface]bool), "&map[F:true]", ""},
+
+ {3, "", `toml: cannot decode to non-pointer "int"`},
+ {map[string]interface{}{}, "", `toml: cannot decode to non-pointer "map[string]interface {}"`},
+
+ {(*int)(nil), "", `toml: cannot decode to nil value of "*int"`},
+ {(*Unmarshaler)(nil), "", `toml: cannot decode to nil value of "*toml.Unmarshaler"`},
+ {nil, "", `toml: cannot decode to non-pointer <nil>`},
+
+ {new(map[int]string), "", "toml: cannot decode to a map with non-string key type"},
+
+ {new(struct{ F int }), "", `toml: line 1 (last key "F"): incompatible types: TOML value has type bool; destination has type integer`},
+ {new(map[string]int), "", `toml: line 1 (last key "F"): incompatible types: TOML value has type bool; destination has type integer`},
+ {new(int), "", `toml: cannot decode to type int`},
+ {new([]int), "", "toml: cannot decode to type []int"},
+ } {
+ t.Run(fmt.Sprintf("%T", tt.v), func(t *testing.T) {
+ _, err := Decode(`F = true`, tt.v)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatalf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
+ }
+
+ if err == nil {
+ have := fmt.Sprintf("%v", tt.v)
+ if n, ok := tt.v.(*NopUnmarshalTOML); ok {
+ have = fmt.Sprintf("%v", *n)
+ }
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ }
+ })
+ }
+}
+
+func TestUnmarshaler(t *testing.T) {
+ var tomlBlob = `
+[dishes.hamboogie]
+name = "Hamboogie with fries"
+price = 10.99
+
+[[dishes.hamboogie.ingredients]]
+name = "Bread Bun"
+
+[[dishes.hamboogie.ingredients]]
+name = "Lettuce"
+
+[[dishes.hamboogie.ingredients]]
+name = "Real Beef Patty"
+
+[[dishes.hamboogie.ingredients]]
+name = "Tomato"
+
+[dishes.eggsalad]
+name = "Egg Salad with rice"
+price = 3.99
+
+[[dishes.eggsalad.ingredients]]
+name = "Egg"
+
+[[dishes.eggsalad.ingredients]]
+name = "Mayo"
+
+[[dishes.eggsalad.ingredients]]
+name = "Rice"
+`
+ m := &menu{}
+ if _, err := Decode(tomlBlob, m); err != nil {
+ t.Fatal(err)
+ }
+
+ if len(m.Dishes) != 2 {
+ t.Log("two dishes should be loaded with UnmarshalTOML()")
+ t.Errorf("expected %d but got %d", 2, len(m.Dishes))
+ }
+
+ eggSalad := m.Dishes["eggsalad"]
+ if _, ok := interface{}(eggSalad).(dish); !ok {
+ t.Errorf("expected a dish")
+ }
+
+ if eggSalad.Name != "Egg Salad with rice" {
+ t.Errorf("expected the dish to be named 'Egg Salad with rice'")
+ }
+
+ if len(eggSalad.Ingredients) != 3 {
+ t.Log("dish should be loaded with UnmarshalTOML()")
+ t.Errorf("expected %d but got %d", 3, len(eggSalad.Ingredients))
+ }
+
+ found := false
+ for _, i := range eggSalad.Ingredients {
+ if i.Name == "Rice" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Rice was not loaded in UnmarshalTOML()")
+ }
+
+ // test on a value - must be passed as *
+ o := menu{}
+ if _, err := Decode(tomlBlob, &o); err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func TestDecodeInlineTable(t *testing.T) {
+ input := `
+[CookieJar]
+Types = {Chocolate = "yummy", Oatmeal = "best ever"}
+
+[Seasons]
+Locations = {NY = {Temp = "not cold", Rating = 4}, MI = {Temp = "freezing", Rating = 9}}
+`
+ type cookieJar struct {
+ Types map[string]string
+ }
+ type properties struct {
+ Temp string
+ Rating int
+ }
+ type seasons struct {
+ Locations map[string]properties
+ }
+ type wrapper struct {
+ CookieJar cookieJar
+ Seasons seasons
+ }
+ var got wrapper
+
+ meta, err := Decode(input, &got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := wrapper{
+ CookieJar: cookieJar{
+ Types: map[string]string{
+ "Chocolate": "yummy",
+ "Oatmeal": "best ever",
+ },
+ },
+ Seasons: seasons{
+ Locations: map[string]properties{
+ "NY": {
+ Temp: "not cold",
+ Rating: 4,
+ },
+ "MI": {
+ Temp: "freezing",
+ Rating: 9,
+ },
+ },
+ },
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("after decode, got:\n\n%#v\n\nwant:\n\n%#v", got, want)
+ }
+ if len(meta.keys) != 12 {
+ t.Errorf("after decode, got %d meta keys; want 12", len(meta.keys))
+ }
+ if len(meta.keyInfo) != 12 {
+ t.Errorf("after decode, got %d meta keyInfo; want 12", len(meta.keyInfo))
+ }
+}
+
+func TestDecodeInlineTableArray(t *testing.T) {
+ type point struct {
+ X, Y, Z int
+ }
+ var got struct {
+ Points []point
+ }
+ // Example inline table array from the spec.
+ const in = `
+points = [ { x = 1, y = 2, z = 3 },
+ { x = 7, y = 8, z = 9 },
+ { x = 2, y = 4, z = 8 } ]
+
+`
+ if _, err := Decode(in, &got); err != nil {
+ t.Fatal(err)
+ }
+ want := []point{
+ {X: 1, Y: 2, Z: 3},
+ {X: 7, Y: 8, Z: 9},
+ {X: 2, Y: 4, Z: 8},
+ }
+ if !reflect.DeepEqual(got.Points, want) {
+ t.Errorf("got %#v; want %#v", got.Points, want)
+ }
+}
+
+type menu struct {
+ Dishes map[string]dish
+}
+
+func (m *menu) UnmarshalTOML(p interface{}) error {
+ m.Dishes = make(map[string]dish)
+ data, _ := p.(map[string]interface{})
+ dishes := data["dishes"].(map[string]interface{})
+ for n, v := range dishes {
+ if d, ok := v.(map[string]interface{}); ok {
+ nd := dish{}
+ nd.UnmarshalTOML(d)
+ m.Dishes[n] = nd
+ } else {
+ return fmt.Errorf("not a dish")
+ }
+ }
+ return nil
+}
+
+type dish struct {
+ Name string
+ Price float32
+ Ingredients []ingredient
+}
+
+func (d *dish) UnmarshalTOML(p interface{}) error {
+ data, _ := p.(map[string]interface{})
+ d.Name, _ = data["name"].(string)
+ d.Price, _ = data["price"].(float32)
+ ingredients, _ := data["ingredients"].([]map[string]interface{})
+ for _, e := range ingredients {
+ n, _ := interface{}(e).(map[string]interface{})
+ name, _ := n["name"].(string)
+ i := ingredient{name}
+ d.Ingredients = append(d.Ingredients, i)
+ }
+ return nil
+}
+
+type ingredient struct {
+ Name string
+}
+
+func TestDecodeSlices(t *testing.T) {
+ type (
+ T struct {
+ Arr []string
+ Tbl map[string]interface{}
+ }
+ M map[string]interface{}
+ )
+ tests := []struct {
+ input string
+ in, want T
+ }{
+ {"",
+ T{}, T{}},
+
+ // Leave existing values alone.
+ {"",
+ T{[]string{}, M{"arr": []string{}}},
+ T{[]string{}, M{"arr": []string{}}}},
+ {"",
+ T{[]string{"a"}, M{"arr": []string{"b"}}},
+ T{[]string{"a"}, M{"arr": []string{"b"}}}},
+
+ // Empty array always allocates (see #339)
+ {`arr = []
+ tbl = {arr = []}`,
+ T{},
+ T{[]string{}, M{"arr": []interface{}{}}}},
+ {`arr = []
+ tbl = {}`,
+ T{[]string{}, M{}},
+ T{[]string{}, M{}}},
+
+ {`arr = []`,
+ T{[]string{"a"}, M{}},
+ T{[]string{}, M{}}},
+
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{[]string{}, M{}},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{[]string{"a", "b"}, M{"arr": []interface{}{"c", "d"}}},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.input, &tt.in)
+ if err != nil {
+ t.Error(err)
+ }
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("\nhave: %#v\nwant: %#v", tt.in, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodePrimitive(t *testing.T) {
+ type S struct {
+ P Primitive
+ }
+ type T struct {
+ S []int
+ }
+ slicep := func(s []int) *[]int { return &s }
+ arrayp := func(a [2]int) *[2]int { return &a }
+ mapp := func(m map[string]int) *map[string]int { return &m }
+ for i, tt := range []struct {
+ v interface{}
+ input string
+ want interface{}
+ }{
+ // slices
+ {slicep(nil), "", slicep(nil)},
+ {slicep([]int{}), "", slicep([]int{})},
+ {slicep([]int{1, 2, 3}), "", slicep([]int{1, 2, 3})},
+ {slicep(nil), "P = [1,2]", slicep([]int{1, 2})},
+ {slicep([]int{}), "P = [1,2]", slicep([]int{1, 2})},
+ {slicep([]int{1, 2, 3}), "P = [1,2]", slicep([]int{1, 2})},
+
+ // arrays
+ {arrayp([2]int{2, 3}), "", arrayp([2]int{2, 3})},
+ {arrayp([2]int{2, 3}), "P = [3,4]", arrayp([2]int{3, 4})},
+
+ // maps
+ {mapp(nil), "", mapp(nil)},
+ {mapp(map[string]int{}), "", mapp(map[string]int{})},
+ {mapp(map[string]int{"a": 1}), "", mapp(map[string]int{"a": 1})},
+ {mapp(nil), "[P]\na = 2", mapp(map[string]int{"a": 2})},
+ {mapp(map[string]int{}), "[P]\na = 2", mapp(map[string]int{"a": 2})},
+ {mapp(map[string]int{"a": 1, "b": 3}), "[P]\na = 2", mapp(map[string]int{"a": 2, "b": 3})},
+
+ // structs
+ {&T{nil}, "[P]", &T{nil}},
+ {&T{[]int{}}, "[P]", &T{[]int{}}},
+ {&T{[]int{1, 2, 3}}, "[P]", &T{[]int{1, 2, 3}}},
+ {&T{nil}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ {&T{[]int{}}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ {&T{[]int{1, 2, 3}}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ } {
+ var s S
+ md, err := Decode(tt.input, &s)
+ if err != nil {
+ t.Errorf("[%d] Decode error: %s", i, err)
+ continue
+ }
+ if err := md.PrimitiveDecode(s.P, tt.v); err != nil {
+ t.Errorf("[%d] PrimitiveDecode error: %s", i, err)
+ continue
+ }
+ if !reflect.DeepEqual(tt.v, tt.want) {
+ t.Errorf("[%d] got %#v; want %#v", i, tt.v, tt.want)
+ }
+ }
+}
+
+func TestDecodeDatetime(t *testing.T) {
+ // Test here in addition to toml-test to ensure the TZs are correct.
+ tz7 := time.FixedZone("", -3600*7)
+
+ for _, tt := range []struct {
+ in string
+ want time.Time
+ }{
+ // Offset datetime
+ {"1979-05-27T07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+ {"1979-05-27T07:32:00.999999Z", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.UTC)},
+ {"1979-05-27T00:32:00-07:00", time.Date(1979, 05, 27, 00, 32, 0, 0, tz7)},
+ {"1979-05-27T00:32:00.999999-07:00", time.Date(1979, 05, 27, 00, 32, 0, 999999000, tz7)},
+ {"1979-05-27T00:32:00.24-07:00", time.Date(1979, 05, 27, 00, 32, 0, 240000000, tz7)},
+ {"1979-05-27 07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+ {"1979-05-27t07:32:00z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+
+ // Make sure the space between the datetime and "#" isn't lexed.
+ {"1979-05-27T07:32:12-07:00 # c", time.Date(1979, 05, 27, 07, 32, 12, 0, tz7)},
+
+ // Local times.
+ {"1979-05-27T07:32:00", time.Date(1979, 05, 27, 07, 32, 0, 0, internal.LocalDatetime)},
+ {"1979-05-27T07:32:00.999999", time.Date(1979, 05, 27, 07, 32, 0, 999999000, internal.LocalDatetime)},
+ {"1979-05-27T07:32:00.25", time.Date(1979, 05, 27, 07, 32, 0, 250000000, internal.LocalDatetime)},
+ {"1979-05-27", time.Date(1979, 05, 27, 0, 0, 0, 0, internal.LocalDate)},
+ {"07:32:00", time.Date(0, 1, 1, 07, 32, 0, 0, internal.LocalTime)},
+ {"07:32:00.999999", time.Date(0, 1, 1, 07, 32, 0, 999999000, internal.LocalTime)},
+ } {
+ t.Run(tt.in, func(t *testing.T) {
+ var x struct{ D time.Time }
+ input := "d = " + tt.in
+ if _, err := Decode(input, &x); err != nil {
+ t.Fatalf("got error: %s", err)
+ }
+
+ if h, w := x.D.Format(time.RFC3339Nano), tt.want.Format(time.RFC3339Nano); h != w {
+ t.Errorf("\nhave: %s\nwant: %s", h, w)
+ }
+ })
+ }
+}
+
+func TestDecodeTextUnmarshaler(t *testing.T) {
+ tests := []struct {
+ name string
+ t interface{}
+ toml string
+ want string
+ }{
+ {
+ "time.Time",
+ struct{ Time time.Time }{},
+ "Time = 1987-07-05T05:45:00Z",
+ "map[Time:1987-07-05 05:45:00 +0000 UTC]",
+ },
+ {
+ "*time.Time",
+ struct{ Time *time.Time }{},
+ "Time = 1988-07-05T05:45:00Z",
+ "map[Time:1988-07-05 05:45:00 +0000 UTC]",
+ },
+ {
+ "map[string]time.Time",
+ struct{ Times map[string]time.Time }{},
+ "Times.one = 1989-07-05T05:45:00Z\nTimes.two = 1990-07-05T05:45:00Z",
+ "map[Times:map[one:1989-07-05 05:45:00 +0000 UTC two:1990-07-05 05:45:00 +0000 UTC]]",
+ },
+ {
+ "map[string]*time.Time",
+ struct{ Times map[string]*time.Time }{},
+ "Times.one = 1989-07-05T05:45:00Z\nTimes.two = 1990-07-05T05:45:00Z",
+ "map[Times:map[one:1989-07-05 05:45:00 +0000 UTC two:1990-07-05 05:45:00 +0000 UTC]]",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := Decode(tt.toml, &tt.t)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%v", tt.t)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeDuration(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ toml, want, wantErr string
+ }{
+ {&struct{ T time.Duration }{}, `t = "0s"`,
+ "&{0s}", ""},
+ {&struct{ T time.Duration }{}, `t = "5m4s"`,
+ "&{5m4s}", ""},
+ {&struct{ T time.Duration }{}, `t = "4.000000002s"`,
+ "&{4.000000002s}", ""},
+
+ {&struct{ T time.Duration }{}, `t = 0`,
+ "&{0s}", ""},
+ {&struct{ T time.Duration }{}, `t = 12345678`,
+ "&{12.345678ms}", ""},
+
+ {&struct{ T *time.Duration }{}, `T = "5s"`,
+ "&{5s}", ""},
+ {&struct{ T *time.Duration }{}, `T = 5`,
+ "&{5ns}", ""},
+
+ {&struct{ T map[string]time.Duration }{}, `T.dur = "5s"`,
+ "&{map[dur:5s]}", ""},
+ {&struct{ T map[string]*time.Duration }{}, `T.dur = "5s"`,
+ "&{map[dur:5s]}", ""},
+
+ {&struct{ T []time.Duration }{}, `T = ["5s"]`,
+ "&{[5s]}", ""},
+ {&struct{ T []*time.Duration }{}, `T = ["5s"]`,
+ "&{[5s]}", ""},
+
+ {&struct{ T time.Duration }{}, `t = "99 bottles of beer"`, "&{0s}", `invalid duration: "99 bottles of beer"`},
+ {&struct{ T time.Duration }{}, `t = "one bottle of beer"`, "&{0s}", `invalid duration: "one bottle of beer"`},
+ {&struct{ T time.Duration }{}, `t = 1.2`, "&{0s}", "incompatible types:"},
+ {&struct{ T time.Duration }{}, `t = {}`, "&{0s}", "incompatible types:"},
+ {&struct{ T time.Duration }{}, `t = []`, "&{0s}", "incompatible types:"},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.toml, tt.in)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%s", tt.in)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeJSONNumber(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ toml, want, wantErr string
+ }{
+ {&struct{ D json.Number }{}, `D = 2`, "&{2}", ""},
+ {&struct{ D json.Number }{}, `D = 2.002`, "&{2.002}", ""},
+ {&struct{ D *json.Number }{}, `D = 2`, "&{2}", ""},
+ {&struct{ D *json.Number }{}, `D = 2.002`, "&{2.002}", ""},
+ {&struct{ D []json.Number }{}, `D = [2, 3.03]`, "&{[2 3.03]}", ""},
+ {&struct{ D []*json.Number }{}, `D = [2, 3.03]`, "&{[2 3.03]}", ""},
+ {&struct{ D map[string]json.Number }{}, `D = {a=2, b=3.03}`, "&{map[a:2 b:3.03]}", ""},
+ {&struct{ D map[string]*json.Number }{}, `D = {a=2, b=3.03}`, "&{map[a:2 b:3.03]}", ""},
+
+ {&struct{ D json.Number }{}, `D = {}`, "&{}", "incompatible types"},
+ {&struct{ D json.Number }{}, `D = []`, "&{}", "incompatible types"},
+ {&struct{ D json.Number }{}, `D = "2"`, "&{}", "incompatible types"},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.toml, tt.in)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%s", tt.in)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestMetaDotConflict(t *testing.T) {
+ var m map[string]interface{}
+ meta, err := Decode(`
+ "a.b" = "str"
+ a.b = 1
+ "" = 2
+ `, &m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `"a.b"=String; a.b=Integer; ""=Integer`
+ have := ""
+ for i, k := range meta.Keys() {
+ if i > 0 {
+ have += "; "
+ }
+ have += k.String() + "=" + meta.Type(k...)
+ }
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+type (
+ Outer struct {
+ Int *InnerInt
+ Enum *Enum
+ Slice *InnerArrayString
+ }
+ Enum int
+ InnerString struct{ value string }
+ InnerInt struct{ value int }
+ InnerBool struct{ value bool }
+ InnerArrayString struct{ value []string }
+)
+
+const (
+ NoValue Enum = iota
+ OtherValue
+)
+
+func (e *Enum) Value() string {
+ switch *e {
+ case OtherValue:
+ return "OTHER_VALUE"
+ }
+ return ""
+}
+
+func (e *Enum) MarshalTOML() ([]byte, error) {
+ return []byte(`"` + e.Value() + `"`), nil
+}
+
+func (e *Enum) UnmarshalTOML(value interface{}) error {
+ sValue, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("value %v is not a string type", value)
+ }
+ for _, enum := range []Enum{NoValue, OtherValue} {
+ if enum.Value() == sValue {
+ *e = enum
+ return nil
+ }
+ }
+ return errors.New("invalid enum value")
+}
+
+func (i *InnerInt) MarshalTOML() ([]byte, error) {
+ return []byte(strconv.Itoa(i.value)), nil
+}
+func (i *InnerInt) UnmarshalTOML(value interface{}) error {
+ iValue, ok := value.(int64)
+ if !ok {
+ return fmt.Errorf("value %v is not a int type", value)
+ }
+ i.value = int(iValue)
+ return nil
+}
+
+func (as *InnerArrayString) MarshalTOML() ([]byte, error) {
+ return []byte("[\"" + strings.Join(as.value, "\", \"") + "\"]"), nil
+}
+
+func (as *InnerArrayString) UnmarshalTOML(value interface{}) error {
+ if value != nil {
+ asValue, ok := value.([]interface{})
+ if !ok {
+ return fmt.Errorf("value %v is not a [] type", value)
+ }
+ as.value = []string{}
+ for _, value := range asValue {
+ as.value = append(as.value, value.(string))
+ }
+ }
+ return nil
+}
+
+// Test for #341
+func TestCustomEncode(t *testing.T) {
+ enum := OtherValue
+ outer := Outer{
+ Int: &InnerInt{value: 10},
+ Enum: &enum,
+ Slice: &InnerArrayString{value: []string{"text1", "text2"}},
+ }
+
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(outer)
+ if err != nil {
+ t.Errorf("Encode failed: %s", err)
+ }
+
+ have := strings.TrimSpace(buf.String())
+ want := strings.ReplaceAll(strings.TrimSpace(`
+ Int = 10
+ Enum = "OTHER_VALUE"
+ Slice = ["text1", "text2"]
+ `), "\t", "")
+ if want != have {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, want)
+ }
+}
+
+// Test for #341
+func TestCustomDecode(t *testing.T) {
+ var outer Outer
+ _, err := Decode(`
+ Int = 10
+ Enum = "OTHER_VALUE"
+ Slice = ["text1", "text2"]
+ `, &outer)
+ if err != nil {
+ t.Fatalf("Decode failed: %s", err)
+ }
+
+ if outer.Int.value != 10 {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Int.value, 10)
+ }
+ if *outer.Enum != OtherValue {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Enum, OtherValue)
+ }
+ if fmt.Sprint(outer.Slice.value) != fmt.Sprint([]string{"text1", "text2"}) {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Slice.value, []string{"text1", "text2"})
+ }
+}
+
+// TODO: this should be improved for v2:
+// https://github.com/BurntSushi/toml/issues/384
+func TestDecodeDoubleTags(t *testing.T) {
+ var s struct {
+ A int `toml:"a"`
+ B int `toml:"a"`
+ C int `toml:"c"`
+ }
+ _, err := Decode(`
+ a = 1
+ b = 2
+ c = 3
+ `, &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `{0 0 3}`
+ have := fmt.Sprintf("%v", s)
+ if want != have {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, want)
+ }
+}
+
+func TestMetaKeys(t *testing.T) {
+ tests := []struct {
+ in string
+ want []Key
+ }{
+ {"", []Key{}},
+ {"b=1\na=1", []Key{Key{"b"}, Key{"a"}}},
+ {"a.b=1\na.a=1", []Key{Key{"a", "b"}, Key{"a", "a"}}}, // TODO: should include "a"
+ {"[tbl]\na=1", []Key{Key{"tbl"}, Key{"tbl", "a"}}},
+ {"[tbl]\na.a=1", []Key{Key{"tbl"}, Key{"tbl", "a", "a"}}}, // TODO: should include "a.a"
+ {"tbl={a=1}", []Key{Key{"tbl"}, Key{"tbl", "a"}}},
+ {"tbl={a={b=1}}", []Key{Key{"tbl"}, Key{"tbl", "a"}, Key{"tbl", "a", "b"}}},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var x interface{}
+ meta, err := Decode(tt.in, &x)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := meta.Keys()
+ if !reflect.DeepEqual(tt.want, have) {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeParallel(t *testing.T) {
+ doc, err := os.ReadFile("testdata/ja-JP.toml")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var wg sync.WaitGroup
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ err := Unmarshal(doc, new(map[string]interface{}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+// errorContains checks if the error message in have contains the text in
+// want.
+//
+// This is safe when have is nil. Use an empty string for want if you want to
+// test that err is nil.
+func errorContains(have error, want string) bool {
+ if have == nil {
+ return want == ""
+ }
+ if want == "" {
+ return false
+ }
+ return strings.Contains(have.Error(), want)
+}
diff --git a/deprecated.go b/deprecated.go
new file mode 100644
index 0000000..b9e3097
--- /dev/null
+++ b/deprecated.go
@@ -0,0 +1,29 @@
+package toml
+
+import (
+ "encoding"
+ "io"
+)
+
+// TextMarshaler is an alias for encoding.TextMarshaler.
+//
+// Deprecated: use encoding.TextMarshaler
+type TextMarshaler encoding.TextMarshaler
+
+// TextUnmarshaler is an alias for encoding.TextUnmarshaler.
+//
+// Deprecated: use encoding.TextUnmarshaler
+type TextUnmarshaler encoding.TextUnmarshaler
+
+// PrimitiveDecode is an alias for MetaData.PrimitiveDecode().
+//
+// Deprecated: use MetaData.PrimitiveDecode.
+func PrimitiveDecode(primValue Primitive, v interface{}) error {
+ md := MetaData{decoded: make(map[string]struct{})}
+ return md.unify(primValue.undecoded, rvalue(v))
+}
+
+// DecodeReader is an alias for NewDecoder(r).Decode(v).
+//
+// Deprecated: use NewDecoder(reader).Decode(&value).
+func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) }
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..81a7c0f
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,11 @@
+// Package toml implements decoding and encoding of TOML files.
+//
+// This package supports TOML v1.0.0, as specified at https://toml.io
+//
+// There is also support for delaying decoding with the Primitive type, and
+// querying the set of keys in a TOML document with the MetaData type.
+//
+// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
+// and can be used to verify if TOML document is valid. It can also be used to
+// print the type of each key.
+package toml
diff --git a/encode.go b/encode.go
new file mode 100644
index 0000000..9cd25d7
--- /dev/null
+++ b/encode.go
@@ -0,0 +1,759 @@
+package toml
+
+import (
+ "bufio"
+ "encoding"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+type tomlEncodeError struct{ error }
+
+var (
+ errArrayNilElement = errors.New("toml: cannot encode array with nil element")
+ errNonString = errors.New("toml: cannot encode a map with non-string key type")
+ errNoKey = errors.New("toml: top-level values must be Go maps or structs")
+ errAnything = errors.New("") // used in testing
+)
+
+var dblQuotedReplacer = strings.NewReplacer(
+ "\"", "\\\"",
+ "\\", "\\\\",
+ "\x00", `\u0000`,
+ "\x01", `\u0001`,
+ "\x02", `\u0002`,
+ "\x03", `\u0003`,
+ "\x04", `\u0004`,
+ "\x05", `\u0005`,
+ "\x06", `\u0006`,
+ "\x07", `\u0007`,
+ "\b", `\b`,
+ "\t", `\t`,
+ "\n", `\n`,
+ "\x0b", `\u000b`,
+ "\f", `\f`,
+ "\r", `\r`,
+ "\x0e", `\u000e`,
+ "\x0f", `\u000f`,
+ "\x10", `\u0010`,
+ "\x11", `\u0011`,
+ "\x12", `\u0012`,
+ "\x13", `\u0013`,
+ "\x14", `\u0014`,
+ "\x15", `\u0015`,
+ "\x16", `\u0016`,
+ "\x17", `\u0017`,
+ "\x18", `\u0018`,
+ "\x19", `\u0019`,
+ "\x1a", `\u001a`,
+ "\x1b", `\u001b`,
+ "\x1c", `\u001c`,
+ "\x1d", `\u001d`,
+ "\x1e", `\u001e`,
+ "\x1f", `\u001f`,
+ "\x7f", `\u007f`,
+)
+
+var (
+ marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
+ marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
+ timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
+)
+
+// Marshaler is the interface implemented by types that can marshal themselves
+// into valid TOML.
+type Marshaler interface {
+ MarshalTOML() ([]byte, error)
+}
+
+// Encoder encodes a Go to a TOML document.
+//
+// The mapping between Go values and TOML values should be precisely the same as
+// for [Decode].
+//
+// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
+// representation.
+//
+// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
+// encoding the value as custom TOML.
+//
+// If you want to write arbitrary binary data then you will need to use
+// something like base64 since TOML does not have any binary types.
+//
+// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
+// are encoded first.
+//
+// Go maps will be sorted alphabetically by key for deterministic output.
+//
+// The toml struct tag can be used to provide the key name; if omitted the
+// struct field name will be used. If the "omitempty" option is present the
+// following value will be skipped:
+//
+// - arrays, slices, maps, and string with len of 0
+// - struct with all zero values
+// - bool false
+//
+// If omitzero is given all int and float types with a value of 0 will be
+// skipped.
+//
+// Encoding Go values without a corresponding TOML representation will return an
+// error. Examples of this includes maps with non-string keys, slices with nil
+// elements, embedded non-struct types, and nested slices containing maps or
+// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
+// is okay, as is []map[string][]string).
+//
+// NOTE: only exported keys are encoded due to the use of reflection. Unexported
+// keys are silently discarded.
+type Encoder struct {
+ // String to use for a single indentation level; default is two spaces.
+ Indent string
+
+ w *bufio.Writer
+ hasWritten bool // written any output to w yet?
+}
+
+// NewEncoder create a new Encoder.
+func NewEncoder(w io.Writer) *Encoder {
+ return &Encoder{
+ w: bufio.NewWriter(w),
+ Indent: " ",
+ }
+}
+
+// Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
+//
+// An error is returned if the value given cannot be encoded to a valid TOML
+// document.
+func (enc *Encoder) Encode(v interface{}) error {
+ rv := eindirect(reflect.ValueOf(v))
+ err := enc.safeEncode(Key([]string{}), rv)
+ if err != nil {
+ return err
+ }
+ return enc.w.Flush()
+}
+
+func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ if terr, ok := r.(tomlEncodeError); ok {
+ err = terr.error
+ return
+ }
+ panic(r)
+ }
+ }()
+ enc.encode(key, rv)
+ return nil
+}
+
+func (enc *Encoder) encode(key Key, rv reflect.Value) {
+ // If we can marshal the type to text, then we use that. This prevents the
+ // encoder for handling these types as generic structs (or whatever the
+ // underlying type of a TextMarshaler is).
+ switch {
+ case isMarshaler(rv):
+ enc.writeKeyValue(key, rv, false)
+ return
+ case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
+ enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
+ return
+ }
+
+ k := rv.Kind()
+ switch k {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
+ reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
+ reflect.Uint64,
+ reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
+ enc.writeKeyValue(key, rv, false)
+ case reflect.Array, reflect.Slice:
+ if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
+ enc.eArrayOfTables(key, rv)
+ } else {
+ enc.writeKeyValue(key, rv, false)
+ }
+ case reflect.Interface:
+ if rv.IsNil() {
+ return
+ }
+ enc.encode(key, rv.Elem())
+ case reflect.Map:
+ if rv.IsNil() {
+ return
+ }
+ enc.eTable(key, rv)
+ case reflect.Ptr:
+ if rv.IsNil() {
+ return
+ }
+ enc.encode(key, rv.Elem())
+ case reflect.Struct:
+ enc.eTable(key, rv)
+ default:
+ encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k))
+ }
+}
+
+// eElement encodes any value that can be an array element.
+func (enc *Encoder) eElement(rv reflect.Value) {
+ switch v := rv.Interface().(type) {
+ case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
+ format := time.RFC3339Nano
+ switch v.Location() {
+ case internal.LocalDatetime:
+ format = "2006-01-02T15:04:05.999999999"
+ case internal.LocalDate:
+ format = "2006-01-02"
+ case internal.LocalTime:
+ format = "15:04:05.999999999"
+ }
+ switch v.Location() {
+ default:
+ enc.wf(v.Format(format))
+ case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
+ enc.wf(v.In(time.UTC).Format(format))
+ }
+ return
+ case Marshaler:
+ s, err := v.MarshalTOML()
+ if err != nil {
+ encPanic(err)
+ }
+ if s == nil {
+ encPanic(errors.New("MarshalTOML returned nil and no error"))
+ }
+ enc.w.Write(s)
+ return
+ case encoding.TextMarshaler:
+ s, err := v.MarshalText()
+ if err != nil {
+ encPanic(err)
+ }
+ if s == nil {
+ encPanic(errors.New("MarshalText returned nil and no error"))
+ }
+ enc.writeQuoted(string(s))
+ return
+ case time.Duration:
+ enc.writeQuoted(v.String())
+ return
+ case json.Number:
+ n, _ := rv.Interface().(json.Number)
+
+ if n == "" { /// Useful zero value.
+ enc.w.WriteByte('0')
+ return
+ } else if v, err := n.Int64(); err == nil {
+ enc.eElement(reflect.ValueOf(v))
+ return
+ } else if v, err := n.Float64(); err == nil {
+ enc.eElement(reflect.ValueOf(v))
+ return
+ }
+ encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
+ }
+
+ switch rv.Kind() {
+ case reflect.Ptr:
+ enc.eElement(rv.Elem())
+ return
+ case reflect.String:
+ enc.writeQuoted(rv.String())
+ case reflect.Bool:
+ enc.wf(strconv.FormatBool(rv.Bool()))
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ enc.wf(strconv.FormatInt(rv.Int(), 10))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ enc.wf(strconv.FormatUint(rv.Uint(), 10))
+ case reflect.Float32:
+ f := rv.Float()
+ if math.IsNaN(f) {
+ enc.wf("nan")
+ } else if math.IsInf(f, 0) {
+ enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
+ } else {
+ enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
+ }
+ case reflect.Float64:
+ f := rv.Float()
+ if math.IsNaN(f) {
+ enc.wf("nan")
+ } else if math.IsInf(f, 0) {
+ enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
+ } else {
+ enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
+ }
+ case reflect.Array, reflect.Slice:
+ enc.eArrayOrSliceElement(rv)
+ case reflect.Struct:
+ enc.eStruct(nil, rv, true)
+ case reflect.Map:
+ enc.eMap(nil, rv, true)
+ case reflect.Interface:
+ enc.eElement(rv.Elem())
+ default:
+ encPanic(fmt.Errorf("unexpected type: %T", rv.Interface()))
+ }
+}
+
+// By the TOML spec, all floats must have a decimal with at least one number on
+// either side.
+func floatAddDecimal(fstr string) string {
+ if !strings.Contains(fstr, ".") {
+ return fstr + ".0"
+ }
+ return fstr
+}
+
+func (enc *Encoder) writeQuoted(s string) {
+ enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
+}
+
+func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
+ length := rv.Len()
+ enc.wf("[")
+ for i := 0; i < length; i++ {
+ elem := eindirect(rv.Index(i))
+ enc.eElement(elem)
+ if i != length-1 {
+ enc.wf(", ")
+ }
+ }
+ enc.wf("]")
+}
+
+func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
+ if len(key) == 0 {
+ encPanic(errNoKey)
+ }
+ for i := 0; i < rv.Len(); i++ {
+ trv := eindirect(rv.Index(i))
+ if isNil(trv) {
+ continue
+ }
+ enc.newline()
+ enc.wf("%s[[%s]]", enc.indentStr(key), key)
+ enc.newline()
+ enc.eMapOrStruct(key, trv, false)
+ }
+}
+
+func (enc *Encoder) eTable(key Key, rv reflect.Value) {
+ if len(key) == 1 {
+ // Output an extra newline between top-level tables.
+ // (The newline isn't written if nothing else has been written though.)
+ enc.newline()
+ }
+ if len(key) > 0 {
+ enc.wf("%s[%s]", enc.indentStr(key), key)
+ enc.newline()
+ }
+ enc.eMapOrStruct(key, rv, false)
+}
+
+func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
+ switch rv.Kind() {
+ case reflect.Map:
+ enc.eMap(key, rv, inline)
+ case reflect.Struct:
+ enc.eStruct(key, rv, inline)
+ default:
+ // Should never happen?
+ panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
+ }
+}
+
+func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
+ rt := rv.Type()
+ if rt.Key().Kind() != reflect.String {
+ encPanic(errNonString)
+ }
+
+ // Sort keys so that we have deterministic output. And write keys directly
+ // underneath this key first, before writing sub-structs or sub-maps.
+ var mapKeysDirect, mapKeysSub []string
+ for _, mapKey := range rv.MapKeys() {
+ k := mapKey.String()
+ if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
+ mapKeysSub = append(mapKeysSub, k)
+ } else {
+ mapKeysDirect = append(mapKeysDirect, k)
+ }
+ }
+
+ var writeMapKeys = func(mapKeys []string, trailC bool) {
+ sort.Strings(mapKeys)
+ for i, mapKey := range mapKeys {
+ val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
+ if isNil(val) {
+ continue
+ }
+
+ if inline {
+ enc.writeKeyValue(Key{mapKey}, val, true)
+ if trailC || i != len(mapKeys)-1 {
+ enc.wf(", ")
+ }
+ } else {
+ enc.encode(key.add(mapKey), val)
+ }
+ }
+ }
+
+ if inline {
+ enc.wf("{")
+ }
+ writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
+ writeMapKeys(mapKeysSub, false)
+ if inline {
+ enc.wf("}")
+ }
+}
+
+const is32Bit = (32 << (^uint(0) >> 63)) == 32
+
+func pointerTo(t reflect.Type) reflect.Type {
+ if t.Kind() == reflect.Ptr {
+ return pointerTo(t.Elem())
+ }
+ return t
+}
+
+func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
+ // Write keys for fields directly under this key first, because if we write
+ // a field that creates a new table then all keys under it will be in that
+ // table (not the one we're writing here).
+ //
+ // Fields is a [][]int: for fieldsDirect this always has one entry (the
+ // struct index). For fieldsSub it contains two entries: the parent field
+ // index from tv, and the field indexes for the fields of the sub.
+ var (
+ rt = rv.Type()
+ fieldsDirect, fieldsSub [][]int
+ addFields func(rt reflect.Type, rv reflect.Value, start []int)
+ )
+ addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
+ for i := 0; i < rt.NumField(); i++ {
+ f := rt.Field(i)
+ isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
+ if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
+ continue
+ }
+ opts := getOptions(f.Tag)
+ if opts.skip {
+ continue
+ }
+
+ frv := eindirect(rv.Field(i))
+
+ if is32Bit {
+ // Copy so it works correct on 32bit archs; not clear why this
+ // is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
+ // This also works fine on 64bit, but 32bit archs are somewhat
+ // rare and this is a wee bit faster.
+ copyStart := make([]int, len(start))
+ copy(copyStart, start)
+ start = copyStart
+ }
+
+ // Treat anonymous struct fields with tag names as though they are
+ // not anonymous, like encoding/json does.
+ //
+ // Non-struct anonymous fields use the normal encoding logic.
+ if isEmbed {
+ if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
+ addFields(frv.Type(), frv, append(start, f.Index...))
+ continue
+ }
+ }
+
+ if typeIsTable(tomlTypeOfGo(frv)) {
+ fieldsSub = append(fieldsSub, append(start, f.Index...))
+ } else {
+ fieldsDirect = append(fieldsDirect, append(start, f.Index...))
+ }
+ }
+ }
+ addFields(rt, rv, nil)
+
+ writeFields := func(fields [][]int) {
+ for _, fieldIndex := range fields {
+ fieldType := rt.FieldByIndex(fieldIndex)
+ fieldVal := rv.FieldByIndex(fieldIndex)
+
+ opts := getOptions(fieldType.Tag)
+ if opts.skip {
+ continue
+ }
+ if opts.omitempty && isEmpty(fieldVal) {
+ continue
+ }
+
+ fieldVal = eindirect(fieldVal)
+
+ if isNil(fieldVal) { /// Don't write anything for nil fields.
+ continue
+ }
+
+ keyName := fieldType.Name
+ if opts.name != "" {
+ keyName = opts.name
+ }
+
+ if opts.omitzero && isZero(fieldVal) {
+ continue
+ }
+
+ if inline {
+ enc.writeKeyValue(Key{keyName}, fieldVal, true)
+ if fieldIndex[0] != len(fields)-1 {
+ enc.wf(", ")
+ }
+ } else {
+ enc.encode(key.add(keyName), fieldVal)
+ }
+ }
+ }
+
+ if inline {
+ enc.wf("{")
+ }
+ writeFields(fieldsDirect)
+ writeFields(fieldsSub)
+ if inline {
+ enc.wf("}")
+ }
+}
+
+// tomlTypeOfGo returns the TOML type name of the Go value's type.
+//
+// It is used to determine whether the types of array elements are mixed (which
+// is forbidden). If the Go value is nil, then it is illegal for it to be an
+// array element, and valueIsNil is returned as true.
+//
+// The type may be `nil`, which means no concrete TOML type could be found.
+func tomlTypeOfGo(rv reflect.Value) tomlType {
+ if isNil(rv) || !rv.IsValid() {
+ return nil
+ }
+
+ if rv.Kind() == reflect.Struct {
+ if rv.Type() == timeType {
+ return tomlDatetime
+ }
+ if isMarshaler(rv) {
+ return tomlString
+ }
+ return tomlHash
+ }
+
+ if isMarshaler(rv) {
+ return tomlString
+ }
+
+ switch rv.Kind() {
+ case reflect.Bool:
+ return tomlBool
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
+ reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
+ reflect.Uint64:
+ return tomlInteger
+ case reflect.Float32, reflect.Float64:
+ return tomlFloat
+ case reflect.Array, reflect.Slice:
+ if isTableArray(rv) {
+ return tomlArrayHash
+ }
+ return tomlArray
+ case reflect.Ptr, reflect.Interface:
+ return tomlTypeOfGo(rv.Elem())
+ case reflect.String:
+ return tomlString
+ case reflect.Map:
+ return tomlHash
+ default:
+ encPanic(errors.New("unsupported type: " + rv.Kind().String()))
+ panic("unreachable")
+ }
+}
+
+func isMarshaler(rv reflect.Value) bool {
+ return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
+}
+
+// isTableArray reports if all entries in the array or slice are a table.
+func isTableArray(arr reflect.Value) bool {
+ if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
+ return false
+ }
+
+ ret := true
+ for i := 0; i < arr.Len(); i++ {
+ tt := tomlTypeOfGo(eindirect(arr.Index(i)))
+ // Don't allow nil.
+ if tt == nil {
+ encPanic(errArrayNilElement)
+ }
+
+ if ret && !typeEqual(tomlHash, tt) {
+ ret = false
+ }
+ }
+ return ret
+}
+
+type tagOptions struct {
+ skip bool // "-"
+ name string
+ omitempty bool
+ omitzero bool
+}
+
+func getOptions(tag reflect.StructTag) tagOptions {
+ t := tag.Get("toml")
+ if t == "-" {
+ return tagOptions{skip: true}
+ }
+ var opts tagOptions
+ parts := strings.Split(t, ",")
+ opts.name = parts[0]
+ for _, s := range parts[1:] {
+ switch s {
+ case "omitempty":
+ opts.omitempty = true
+ case "omitzero":
+ opts.omitzero = true
+ }
+ }
+ return opts
+}
+
+func isZero(rv reflect.Value) bool {
+ switch rv.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return rv.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return rv.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return rv.Float() == 0.0
+ }
+ return false
+}
+
+func isEmpty(rv reflect.Value) bool {
+ switch rv.Kind() {
+ case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
+ return rv.Len() == 0
+ case reflect.Struct:
+ if rv.Type().Comparable() {
+ return reflect.Zero(rv.Type()).Interface() == rv.Interface()
+ }
+ // Need to also check if all the fields are empty, otherwise something
+ // like this with uncomparable types will always return true:
+ //
+ // type a struct{ field b }
+ // type b struct{ s []string }
+ // s := a{field: b{s: []string{"AAA"}}}
+ for i := 0; i < rv.NumField(); i++ {
+ if !isEmpty(rv.Field(i)) {
+ return false
+ }
+ }
+ return true
+ case reflect.Bool:
+ return !rv.Bool()
+ case reflect.Ptr:
+ return rv.IsNil()
+ }
+ return false
+}
+
+func (enc *Encoder) newline() {
+ if enc.hasWritten {
+ enc.wf("\n")
+ }
+}
+
+// Write a key/value pair:
+//
+// key = <any value>
+//
+// This is also used for "k = v" in inline tables; so something like this will
+// be written in three calls:
+//
+// ┌───────────────────┐
+// │ ┌───┐ ┌────┐│
+// v v v v vv
+// key = {k = 1, k2 = 2}
+func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
+ /// Marshaler used on top-level document; call eElement() to just call
+ /// Marshal{TOML,Text}.
+ if len(key) == 0 {
+ enc.eElement(val)
+ return
+ }
+ enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
+ enc.eElement(val)
+ if !inline {
+ enc.newline()
+ }
+}
+
+func (enc *Encoder) wf(format string, v ...interface{}) {
+ _, err := fmt.Fprintf(enc.w, format, v...)
+ if err != nil {
+ encPanic(err)
+ }
+ enc.hasWritten = true
+}
+
+func (enc *Encoder) indentStr(key Key) string {
+ return strings.Repeat(enc.Indent, len(key)-1)
+}
+
+func encPanic(err error) {
+ panic(tomlEncodeError{err})
+}
+
+// Resolve any level of pointers to the actual value (e.g. **string → string).
+func eindirect(v reflect.Value) reflect.Value {
+ if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
+ if isMarshaler(v) {
+ return v
+ }
+ if v.CanAddr() { /// Special case for marshalers; see #358.
+ if pv := v.Addr(); isMarshaler(pv) {
+ return pv
+ }
+ }
+ return v
+ }
+
+ if v.IsNil() {
+ return v
+ }
+
+ return eindirect(v.Elem())
+}
+
+func isNil(rv reflect.Value) bool {
+ switch rv.Kind() {
+ case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
+ return rv.IsNil()
+ default:
+ return false
+ }
+}
diff --git a/encode_test.go b/encode_test.go
new file mode 100644
index 0000000..53e5901
--- /dev/null
+++ b/encode_test.go
@@ -0,0 +1,1326 @@
+package toml
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "math"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestEncodeRoundTrip(t *testing.T) {
+ type Config struct {
+ Age int
+ Cats []string
+ Pi float64
+ Perfection []int
+ DOB time.Time
+ Ipaddress net.IP
+ }
+
+ var inputs = Config{
+ Age: 13,
+ Cats: []string{"one", "two", "three"},
+ Pi: 3.145,
+ Perfection: []int{11, 2, 3, 4},
+ DOB: time.Now(),
+ Ipaddress: net.ParseIP("192.168.59.254"),
+ }
+
+ var (
+ firstBuffer bytes.Buffer
+ secondBuffer bytes.Buffer
+ outputs Config
+ )
+ err := NewEncoder(&firstBuffer).Encode(inputs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = Decode(firstBuffer.String(), &outputs)
+ if err != nil {
+ t.Logf("Could not decode:\n%s\n", firstBuffer.String())
+ t.Fatal(err)
+ }
+ err = NewEncoder(&secondBuffer).Encode(outputs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if firstBuffer.String() != secondBuffer.String() {
+ t.Errorf("%s\n\nIS NOT IDENTICAL TO\n\n%s", firstBuffer.String(), secondBuffer.String())
+ }
+}
+
+func TestEncodeNestedTableArrays(t *testing.T) {
+ type song struct {
+ Name string `toml:"name"`
+ }
+ type album struct {
+ Name string `toml:"name"`
+ Songs []song `toml:"songs"`
+ }
+ type springsteen struct {
+ Albums []album `toml:"albums"`
+ }
+ value := springsteen{
+ []album{
+ {"Born to Run",
+ []song{{"Jungleland"}, {"Meeting Across the River"}}},
+ {"Born in the USA",
+ []song{{"Glory Days"}, {"Dancing in the Dark"}}},
+ },
+ }
+ expected := `[[albums]]
+ name = "Born to Run"
+
+ [[albums.songs]]
+ name = "Jungleland"
+
+ [[albums.songs]]
+ name = "Meeting Across the River"
+
+[[albums]]
+ name = "Born in the USA"
+
+ [[albums.songs]]
+ name = "Glory Days"
+
+ [[albums.songs]]
+ name = "Dancing in the Dark"
+`
+ encodeExpected(t, "nested table arrays", value, expected, nil)
+}
+
+func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) {
+ type Alpha struct {
+ V int
+ }
+ type Beta struct {
+ V int
+ }
+ type Conf struct {
+ V int
+ A Alpha
+ B []Beta
+ }
+
+ val := Conf{
+ V: 1,
+ A: Alpha{2},
+ B: []Beta{{3}},
+ }
+ expected := "V = 1\n\n[A]\n V = 2\n\n[[B]]\n V = 3\n"
+ encodeExpected(t, "array hash with normal hash order", val, expected, nil)
+}
+
+func TestEncodeOmitEmptyStruct(t *testing.T) {
+ type (
+ T struct{ Int int }
+ Tpriv struct {
+ Int int
+ private int
+ }
+ Ttime struct {
+ Time time.Time
+ }
+ )
+
+ tests := []struct {
+ in interface{}
+ want string
+ }{
+ {struct {
+ F T `toml:"f,omitempty"`
+ }{}, ""},
+ {struct {
+ F T `toml:"f,omitempty"`
+ }{T{1}}, "[f]\n Int = 1"},
+
+ {struct {
+ F Tpriv `toml:"f,omitempty"`
+ }{}, ""},
+ {struct {
+ F Tpriv `toml:"f,omitempty"`
+ }{Tpriv{1, 0}}, "[f]\n Int = 1"},
+
+ // Private field being set also counts as "not empty".
+ {struct {
+ F Tpriv `toml:"f,omitempty"`
+ }{Tpriv{0, 1}}, "[f]\n Int = 0"},
+
+ // time.Time is common use case, so test that explicitly.
+ {struct {
+ F Ttime `toml:"t,omitempty"`
+ }{}, ""},
+ {struct {
+ F Ttime `toml:"t,omitempty"`
+ }{Ttime{time.Time{}.Add(1)}}, "[t]\n Time = 0001-01-01T00:00:00.000000001Z"},
+
+ // TODO: also test with MarshalText, MarshalTOML returning non-zero
+ // value.
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ buf := new(bytes.Buffer)
+
+ err := NewEncoder(buf).Encode(tt.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := strings.TrimSpace(buf.String())
+ if have != tt.want {
+ t.Errorf("\nhave:\n%s\nwant:\n%s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestEncodeOmitEmpty(t *testing.T) {
+ type compareable struct {
+ Bool bool `toml:"bool,omitempty"`
+ }
+ type uncomparable struct {
+ Field []string `toml:"field,omitempty"`
+ }
+ type nestedUncomparable struct {
+ Field uncomparable `toml:"uncomparable,omitempty"`
+ Bool bool `toml:"bool,omitempty"`
+ }
+ type simple struct {
+ Bool bool `toml:"bool,omitempty"`
+ String string `toml:"string,omitempty"`
+ Array [0]byte `toml:"array,omitempty"`
+ Slice []int `toml:"slice,omitempty"`
+ Map map[string]string `toml:"map,omitempty"`
+ Time time.Time `toml:"time,omitempty"`
+ Compareable1 compareable `toml:"compareable1,omitempty"`
+ Compareable2 compareable `toml:"compareable2,omitempty"`
+ Uncomparable1 uncomparable `toml:"uncomparable1,omitempty"`
+ Uncomparable2 uncomparable `toml:"uncomparable2,omitempty"`
+ NestedUncomparable1 nestedUncomparable `toml:"nesteduncomparable1,omitempty"`
+ NestedUncomparable2 nestedUncomparable `toml:"nesteduncomparable2,omitempty"`
+ }
+
+ var v simple
+ encodeExpected(t, "fields with omitempty are omitted when empty", v, "", nil)
+ v = simple{
+ Bool: true,
+ String: " ",
+ Slice: []int{2, 3, 4},
+ Map: map[string]string{"foo": "bar"},
+ Time: time.Date(1985, 6, 18, 15, 16, 17, 0, time.UTC),
+ Compareable2: compareable{true},
+ Uncomparable2: uncomparable{[]string{"XXX"}},
+ NestedUncomparable1: nestedUncomparable{uncomparable{[]string{"XXX"}}, false},
+ NestedUncomparable2: nestedUncomparable{uncomparable{}, true},
+ }
+ expected := `bool = true
+string = " "
+slice = [2, 3, 4]
+time = 1985-06-18T15:16:17Z
+
+[map]
+ foo = "bar"
+
+[compareable2]
+ bool = true
+
+[uncomparable2]
+ field = ["XXX"]
+
+[nesteduncomparable1]
+ [nesteduncomparable1.uncomparable]
+ field = ["XXX"]
+
+[nesteduncomparable2]
+ bool = true
+`
+ encodeExpected(t, "fields with omitempty are not omitted when non-empty",
+ v, expected, nil)
+}
+
+func TestEncodeOmitEmptyPointer(t *testing.T) {
+ type s struct {
+ String *string `toml:"string,omitempty"`
+ }
+
+ t.Run("nil pointers", func(t *testing.T) {
+ var v struct {
+ String *string `toml:"string,omitempty"`
+ Slice *[]string `toml:"slice,omitempty"`
+ Map *map[string]string `toml:"map,omitempty"`
+ Struct *s `toml:"struct,omitempty"`
+ }
+ encodeExpected(t, "", v, ``, nil)
+ })
+
+ t.Run("zero values", func(t *testing.T) {
+ str := ""
+ sl := []string{}
+ m := map[string]string{}
+
+ v := struct {
+ String *string `toml:"string,omitempty"`
+ Slice *[]string `toml:"slice,omitempty"`
+ Map *map[string]string `toml:"map,omitempty"`
+ Struct *s `toml:"struct,omitempty"`
+ }{&str, &sl, &m, &s{&str}}
+ want := `string = ""
+slice = []
+
+[map]
+
+[struct]
+ string = ""
+`
+ encodeExpected(t, "", v, want, nil)
+ })
+
+ t.Run("with values", func(t *testing.T) {
+ str := "XXX"
+ sl := []string{"XXX"}
+ m := map[string]string{"XXX": "XXX"}
+
+ v := struct {
+ String *string `toml:"string,omitempty"`
+ Slice *[]string `toml:"slice,omitempty"`
+ Map *map[string]string `toml:"map,omitempty"`
+ Struct *s `toml:"struct,omitempty"`
+ }{&str, &sl, &m, &s{&str}}
+ want := `string = "XXX"
+slice = ["XXX"]
+
+[map]
+ XXX = "XXX"
+
+[struct]
+ string = "XXX"
+`
+ encodeExpected(t, "", v, want, nil)
+ })
+}
+
+func TestEncodeOmitZero(t *testing.T) {
+ type simple struct {
+ Number int `toml:"number,omitzero"`
+ Real float64 `toml:"real,omitzero"`
+ Unsigned uint `toml:"unsigned,omitzero"`
+ }
+
+ value := simple{0, 0.0, uint(0)}
+ expected := ""
+
+ encodeExpected(t, "simple with omitzero, all zero", value, expected, nil)
+
+ value.Number = 10
+ value.Real = 20
+ value.Unsigned = 5
+ expected = `number = 10
+real = 20.0
+unsigned = 5
+`
+ encodeExpected(t, "simple with omitzero, non-zero", value, expected, nil)
+}
+
+func TestEncodeOmitemptyEmptyName(t *testing.T) {
+ type simple struct {
+ S []int `toml:",omitempty"`
+ }
+ v := simple{[]int{1, 2, 3}}
+ expected := "S = [1, 2, 3]\n"
+ encodeExpected(t, "simple with omitempty, no name, non-empty field",
+ v, expected, nil)
+}
+
+func TestEncodeAnonymousStruct(t *testing.T) {
+ type Inner struct{ N int }
+ type inner struct{ B int }
+ type Embedded struct {
+ Inner1 Inner
+ Inner2 Inner
+ }
+ type Outer0 struct {
+ Inner
+ inner
+ }
+ type Outer1 struct {
+ Inner `toml:"inner"`
+ inner `toml:"innerb"`
+ }
+ type Outer3 struct {
+ Embedded
+ }
+
+ v0 := Outer0{Inner{3}, inner{4}}
+ expected := "N = 3\nB = 4\n"
+ encodeExpected(t, "embedded anonymous untagged struct", v0, expected, nil)
+
+ v1 := Outer1{Inner{3}, inner{4}}
+ expected = "[inner]\n N = 3\n\n[innerb]\n B = 4\n"
+ encodeExpected(t, "embedded anonymous tagged struct", v1, expected, nil)
+
+ v3 := Outer3{Embedded: Embedded{Inner{3}, Inner{4}}}
+ expected = "[Inner1]\n N = 3\n\n[Inner2]\n N = 4\n"
+ encodeExpected(t, "embedded anonymous multiple fields", v3, expected, nil)
+}
+
+func TestEncodeAnonymousStructPointerField(t *testing.T) {
+ type Inner struct{ N int }
+ type Outer0 struct{ *Inner }
+ type Outer1 struct {
+ *Inner `toml:"inner"`
+ }
+
+ v0 := Outer0{}
+ expected := ""
+ encodeExpected(t, "nil anonymous untagged struct pointer field", v0, expected, nil)
+
+ v0 = Outer0{&Inner{3}}
+ expected = "N = 3\n"
+ encodeExpected(t, "non-nil anonymous untagged struct pointer field", v0, expected, nil)
+
+ v1 := Outer1{}
+ expected = ""
+ encodeExpected(t, "nil anonymous tagged struct pointer field", v1, expected, nil)
+
+ v1 = Outer1{&Inner{3}}
+ expected = "[inner]\n N = 3\n"
+ encodeExpected(t, "non-nil anonymous tagged struct pointer field", v1, expected, nil)
+}
+
+func TestEncodeNestedAnonymousStructs(t *testing.T) {
+ type A struct{ A string }
+ type B struct{ B string }
+ type C struct{ C string }
+ type BC struct {
+ B
+ C
+ }
+ type Outer struct {
+ A
+ BC
+ }
+
+ v := &Outer{
+ A: A{
+ A: "a",
+ },
+ BC: BC{
+ B: B{
+ B: "b",
+ },
+ C: C{
+ C: "c",
+ },
+ },
+ }
+
+ expected := "A = \"a\"\nB = \"b\"\nC = \"c\"\n"
+ encodeExpected(t, "nested anonymous untagged structs", v, expected, nil)
+}
+
+type InnerForNextTest struct{ N int }
+
+func (InnerForNextTest) F() {}
+func (InnerForNextTest) G() {}
+
+func TestEncodeAnonymousNoStructField(t *testing.T) {
+ type Inner interface{ F() }
+ type inner interface{ G() }
+ type IntS []int
+ type intS []int
+ type Outer0 struct {
+ Inner
+ inner
+ IntS
+ intS
+ }
+
+ v0 := Outer0{
+ Inner: InnerForNextTest{3},
+ inner: InnerForNextTest{4},
+ IntS: []int{5, 6},
+ intS: []int{7, 8},
+ }
+ expected := "IntS = [5, 6]\n\n[Inner]\n N = 3\n"
+ encodeExpected(t, "non struct anonymous field", v0, expected, nil)
+}
+
+func TestEncodeIgnoredFields(t *testing.T) {
+ type simple struct {
+ Number int `toml:"-"`
+ }
+ value := simple{}
+ expected := ""
+ encodeExpected(t, "ignored field", value, expected, nil)
+}
+
+func TestEncodeNaN(t *testing.T) {
+ s1 := struct {
+ Nan float64 `toml:"nan"`
+ Inf float64 `toml:"inf"`
+ }{math.NaN(), math.Inf(1)}
+ s2 := struct {
+ Nan float32 `toml:"nan"`
+ Inf float32 `toml:"inf"`
+ }{float32(math.NaN()), float32(math.Inf(-1))}
+ encodeExpected(t, "", s1, "nan = nan\ninf = +inf\n", nil)
+ encodeExpected(t, "", s2, "nan = nan\ninf = -inf\n", nil)
+}
+
+func TestEncodePrimitive(t *testing.T) {
+ type MyStruct struct {
+ Data Primitive
+ DataA int
+ DataB string
+ }
+
+ decodeAndEncode := func(toml string) string {
+ var s MyStruct
+ _, err := Decode(toml, &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var buf bytes.Buffer
+ err = NewEncoder(&buf).Encode(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return buf.String()
+ }
+
+ original := `DataA = 1
+DataB = "bbb"
+Data = ["Foo", "Bar"]
+`
+ reEncoded := decodeAndEncode(decodeAndEncode(original))
+
+ if reEncoded != original {
+ t.Errorf(
+ "re-encoded not the same as original\noriginal: %q\nre-encoded: %q",
+ original, reEncoded)
+ }
+}
+
+func TestEncodeError(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ wantErr string
+ }{
+ {make(chan int), "unsupported type for key '': chan"},
+ {struct{ C complex128 }{0}, "unsupported type: complex128"},
+ {[]complex128{0}, "unsupported type: complex128"},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ err := NewEncoder(os.Stderr).Encode(tt.in)
+ if err == nil {
+ t.Fatal("err is nil")
+ }
+ if !errorContains(err, tt.wantErr) {
+ t.Errorf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+type (
+ sound struct{ S string }
+ food struct{ F []string }
+ fun func()
+ cplx complex128
+ ints []int
+
+ sound2 struct{ S string }
+ food2 struct{ F []string }
+ fun2 func()
+ cplx2 complex128
+ ints2 []int
+)
+
+// This is intentionally wrong (pointer receiver)
+func (s *sound) MarshalText() ([]byte, error) { return []byte(s.S), nil }
+func (f food) MarshalText() ([]byte, error) { return []byte(strings.Join(f.F, ", ")), nil }
+func (f fun) MarshalText() ([]byte, error) { return []byte("why would you do this?"), nil }
+func (c cplx) MarshalText() ([]byte, error) {
+ cplx := complex128(c)
+ return []byte(fmt.Sprintf("(%f+%fi)", real(cplx), imag(cplx))), nil
+}
+
+func intsValue(is []int) []byte {
+ var buf bytes.Buffer
+ buf.WriteByte('<')
+ for i, v := range is {
+ if i > 0 {
+ buf.WriteByte(',')
+ }
+ buf.WriteString(strconv.Itoa(v))
+ }
+ buf.WriteByte('>')
+ return buf.Bytes()
+}
+
+func (is *ints) MarshalText() ([]byte, error) {
+ if is == nil {
+ return []byte("[]"), nil
+ }
+ return intsValue(*is), nil
+}
+
+func (s *sound2) MarshalTOML() ([]byte, error) { return []byte("\"" + s.S + "\""), nil }
+func (f food2) MarshalTOML() ([]byte, error) {
+ return []byte("[\"" + strings.Join(f.F, "\", \"") + "\"]"), nil
+}
+func (f fun2) MarshalTOML() ([]byte, error) { return []byte("\"why would you do this?\""), nil }
+func (c cplx2) MarshalTOML() ([]byte, error) {
+ cplx := complex128(c)
+ return []byte(fmt.Sprintf("\"(%f+%fi)\"", real(cplx), imag(cplx))), nil
+}
+func (is *ints2) MarshalTOML() ([]byte, error) {
+ // MarshalTOML must quote by self
+ if is == nil {
+ return []byte(`"[]"`), nil
+ }
+ return []byte(fmt.Sprintf(`"%s"`, intsValue(*is))), nil
+}
+
+func TestEncodeTextMarshaler(t *testing.T) {
+ x := struct {
+ Name string
+ Labels map[string]string
+ Sound sound
+ Sound2 *sound
+ Food food
+ Food2 *food
+ Complex cplx
+ Fun fun
+ Ints ints
+ Ints2 *ints2
+ }{
+ Name: "Goblok",
+ Sound: sound{"miauw"},
+ Sound2: &sound{"miauw"},
+ Labels: map[string]string{
+ "type": "cat",
+ "color": "black",
+ },
+ Food: food{[]string{"chicken", "fish"}},
+ Food2: &food{[]string{"chicken", "fish"}},
+ Complex: complex(42, 666),
+ Fun: func() { panic("x") },
+ Ints: ints{1, 2, 3, 4},
+ Ints2: &ints2{1, 2, 3, 4},
+ }
+
+ var buf bytes.Buffer
+ if err := NewEncoder(&buf).Encode(&x); err != nil {
+ t.Fatal(err)
+ }
+
+ want := `Name = "Goblok"
+Sound = "miauw"
+Sound2 = "miauw"
+Food = "chicken, fish"
+Food2 = "chicken, fish"
+Complex = "(42.000000+666.000000i)"
+Fun = "why would you do this?"
+Ints = "<1,2,3,4>"
+Ints2 = "<1,2,3,4>"
+
+[Labels]
+ color = "black"
+ type = "cat"
+`
+
+ if buf.String() != want {
+ t.Error("\n" + buf.String())
+ }
+}
+
+func TestEncodeTOMLMarshaler(t *testing.T) {
+ x := struct {
+ Name string
+ Labels map[string]string
+ Sound sound2
+ Sound2 *sound2
+ Food food2
+ Food2 *food2
+ Complex cplx2
+ Fun fun2
+ }{
+ Name: "Goblok",
+ Sound: sound2{"miauw"},
+ Sound2: &sound2{"miauw"},
+ Labels: map[string]string{
+ "type": "cat",
+ "color": "black",
+ },
+ Food: food2{[]string{"chicken", "fish"}},
+ Food2: &food2{[]string{"chicken", "fish"}},
+ Complex: complex(42, 666),
+ Fun: func() { panic("x") },
+ }
+
+ var buf bytes.Buffer
+ if err := NewEncoder(&buf).Encode(x); err != nil {
+ t.Fatal(err)
+ }
+
+ want := `Name = "Goblok"
+Sound2 = "miauw"
+Food = ["chicken", "fish"]
+Food2 = ["chicken", "fish"]
+Complex = "(42.000000+666.000000i)"
+Fun = "why would you do this?"
+
+[Labels]
+ color = "black"
+ type = "cat"
+
+[Sound]
+ S = "miauw"
+`
+
+ if buf.String() != want {
+ t.Error("\n" + buf.String())
+ }
+}
+
+type (
+ retNil1 string
+ retNil2 string
+)
+
+func (r retNil1) MarshalText() ([]byte, error) { return nil, nil }
+func (r retNil2) MarshalTOML() ([]byte, error) { return nil, nil }
+
+func TestEncodeEmpty(t *testing.T) {
+ t.Run("text", func(t *testing.T) {
+ var (
+ s struct{ Text retNil1 }
+ buf bytes.Buffer
+ )
+ err := NewEncoder(&buf).Encode(s)
+ if err == nil {
+ t.Fatalf("no error, but expected an error; output:\n%s", buf.String())
+ }
+ if buf.String() != "" {
+ t.Error("\n" + buf.String())
+ }
+ })
+
+ t.Run("toml", func(t *testing.T) {
+ var (
+ s struct{ Text retNil2 }
+ buf bytes.Buffer
+ )
+ err := NewEncoder(&buf).Encode(s)
+ if err == nil {
+ t.Fatalf("no error, but expected an error; output:\n%s", buf.String())
+ }
+ if buf.String() != "" {
+ t.Error("\n" + buf.String())
+ }
+ })
+}
+
+// Would previously fail on 32bit architectures; can test with:
+//
+// GOARCH=386 go test -c && ./toml.test
+// GOARCH=arm GOARM=7 go test -c && qemu-arm ./toml.test
+func TestEncode32bit(t *testing.T) {
+ type Inner struct {
+ A, B, C string
+ }
+ type Outer struct{ Inner }
+
+ encodeExpected(t, "embedded anonymous untagged struct",
+ Outer{Inner{"a", "b", "c"}},
+ "A = \"a\"\nB = \"b\"\nC = \"c\"\n",
+ nil)
+}
+
+// Skip invalid types if it has toml:"-"
+//
+// https://github.com/BurntSushi/toml/issues/345
+func TestEncodeSkipInvalidType(t *testing.T) {
+ buf := new(bytes.Buffer)
+ err := NewEncoder(buf).Encode(struct {
+ Str string `toml:"str"`
+ Arr []func() `toml:"-"`
+ Map map[string]interface{} `toml:"-"`
+ Func func() `toml:"-"`
+ }{
+ Str: "a",
+ Arr: []func(){func() {}},
+ Map: map[string]interface{}{"f": func() {}},
+ Func: func() {},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := buf.String()
+ want := "str = \"a\"\n"
+ if have != want {
+ t.Errorf("\nwant: %q\nhave: %q\n", want, have)
+ }
+}
+
+func TestEncodeDuration(t *testing.T) {
+ tests := []time.Duration{
+ 0,
+ time.Second,
+ time.Minute,
+ time.Hour,
+ 248*time.Hour + 45*time.Minute + 24*time.Second,
+ 12345678 * time.Nanosecond,
+ 12345678 * time.Second,
+ 4*time.Second + 2*time.Nanosecond,
+ }
+
+ for _, tt := range tests {
+ encodeExpected(t, tt.String(),
+ struct{ Dur time.Duration }{Dur: tt},
+ fmt.Sprintf("Dur = %q", tt), nil)
+ }
+}
+
+type jsonT struct {
+ Num json.Number
+ NumP *json.Number
+ Arr []json.Number
+ ArrP []*json.Number
+ Tbl map[string]json.Number
+ TblP map[string]*json.Number
+}
+
+var (
+ n2, n4, n6 = json.Number("2"), json.Number("4"), json.Number("6")
+ f2, f4, f6 = json.Number("2.2"), json.Number("4.4"), json.Number("6.6")
+)
+
+func TestEncodeJSONNumber(t *testing.T) {
+ tests := []struct {
+ in jsonT
+ want string
+ }{
+ {jsonT{}, "Num = 0"},
+ {jsonT{
+ Num: "1",
+ NumP: &n2,
+ Arr: []json.Number{"3"},
+ ArrP: []*json.Number{&n4},
+ Tbl: map[string]json.Number{"k1": "5"},
+ TblP: map[string]*json.Number{"k2": &n6}}, `
+ Num = 1
+ NumP = 2
+ Arr = [3]
+ ArrP = [4]
+
+ [Tbl]
+ k1 = 5
+
+ [TblP]
+ k2 = 6
+ `},
+ {jsonT{
+ Num: "1.1",
+ NumP: &f2,
+ Arr: []json.Number{"3.3"},
+ ArrP: []*json.Number{&f4},
+ Tbl: map[string]json.Number{"k1": "5.5"},
+ TblP: map[string]*json.Number{"k2": &f6}}, `
+ Num = 1.1
+ NumP = 2.2
+ Arr = [3.3]
+ ArrP = [4.4]
+
+ [Tbl]
+ k1 = 5.5
+
+ [TblP]
+ k2 = 6.6
+ `},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(tt.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := strings.TrimSpace(buf.String())
+ want := strings.ReplaceAll(strings.TrimSpace(tt.want), "\t", "")
+ if have != want {
+ t.Errorf("\nwant:\n%s\nhave:\n%s\n", want, have)
+ }
+ })
+ }
+}
+
+func TestEncode(t *testing.T) {
+ type Embedded struct {
+ Int int `toml:"_int"`
+ }
+ type NonStruct int
+
+ date := time.Date(2014, 5, 11, 19, 30, 40, 0, time.UTC)
+ dateStr := "2014-05-11T19:30:40Z"
+
+ tests := map[string]struct {
+ input interface{}
+ wantOutput string
+ wantError error
+ }{
+ "bool field": {
+ input: struct {
+ BoolTrue bool
+ BoolFalse bool
+ }{true, false},
+ wantOutput: "BoolTrue = true\nBoolFalse = false\n",
+ },
+ "int fields": {
+ input: struct {
+ Int int
+ Int8 int8
+ Int16 int16
+ Int32 int32
+ Int64 int64
+ }{1, 2, 3, 4, 5},
+ wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n",
+ },
+ "uint fields": {
+ input: struct {
+ Uint uint
+ Uint8 uint8
+ Uint16 uint16
+ Uint32 uint32
+ Uint64 uint64
+ }{1, 2, 3, 4, 5},
+ wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" +
+ "\nUint64 = 5\n",
+ },
+ "float fields": {
+ input: struct {
+ Float32 float32
+ Float64 float64
+ }{1.5, 2.5},
+ wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n",
+ },
+ "string field": {
+ input: struct{ String string }{"foo"},
+ wantOutput: "String = \"foo\"\n",
+ },
+ "string field with \\n escape": {
+ input: struct{ String string }{"foo\n"},
+ wantOutput: "String = \"foo\\n\"\n",
+ },
+ "string field and unexported field": {
+ input: struct {
+ String string
+ unexported int
+ }{"foo", 0},
+ wantOutput: "String = \"foo\"\n",
+ },
+ "datetime field in UTC": {
+ input: struct{ Date time.Time }{date},
+ wantOutput: fmt.Sprintf("Date = %s\n", dateStr),
+ },
+ "datetime field as primitive": {
+ // Using a map here to fail if isStructOrMap() returns true for
+ // time.Time.
+ input: map[string]interface{}{
+ "Date": date,
+ "Int": 1,
+ },
+ wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr),
+ },
+ "array fields": {
+ input: struct {
+ IntArray0 [0]int
+ IntArray3 [3]int
+ }{[0]int{}, [3]int{1, 2, 3}},
+ wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n",
+ },
+ "slice fields": {
+ input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{
+ nil, []int{}, []int{1, 2, 3},
+ },
+ wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n",
+ },
+ "datetime slices": {
+ input: struct{ DatetimeSlice []time.Time }{
+ []time.Time{date, date},
+ },
+ wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n",
+ dateStr, dateStr),
+ },
+ "nested arrays and slices": {
+ input: struct {
+ SliceOfArrays [][2]int
+ ArrayOfSlices [2][]int
+ SliceOfArraysOfSlices [][2][]int
+ ArrayOfSlicesOfArrays [2][][2]int
+ SliceOfMixedArrays [][2]interface{}
+ ArrayOfMixedSlices [2][]interface{}
+ }{
+ [][2]int{{1, 2}, {3, 4}},
+ [2][]int{{1, 2}, {3, 4}},
+ [][2][]int{
+ {
+ {1, 2}, {3, 4},
+ },
+ {
+ {5, 6}, {7, 8},
+ },
+ },
+ [2][][2]int{
+ {
+ {1, 2}, {3, 4},
+ },
+ {
+ {5, 6}, {7, 8},
+ },
+ },
+ [][2]interface{}{
+ {1, 2}, {"a", "b"},
+ },
+ [2][]interface{}{
+ {1, 2}, {"a", "b"},
+ },
+ },
+ wantOutput: `SliceOfArrays = [[1, 2], [3, 4]]
+ArrayOfSlices = [[1, 2], [3, 4]]
+SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+SliceOfMixedArrays = [[1, 2], ["a", "b"]]
+ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
+`,
+ },
+ "empty slice": {
+ input: struct{ Empty []interface{} }{[]interface{}{}},
+ wantOutput: "Empty = []\n",
+ },
+ "(error) slice with element type mismatch (string and integer)": {
+ input: struct{ Mixed []interface{} }{[]interface{}{1, "a"}},
+ wantOutput: "Mixed = [1, \"a\"]\n",
+ },
+ "(error) slice with element type mismatch (integer and float)": {
+ input: struct{ Mixed []interface{} }{[]interface{}{1, 2.5}},
+ wantOutput: "Mixed = [1, 2.5]\n",
+ },
+ "slice with elems of differing Go types, same TOML types": {
+ input: struct {
+ MixedInts []interface{}
+ MixedFloats []interface{}
+ }{
+ []interface{}{
+ int(1), int8(2), int16(3), int32(4), int64(5),
+ uint(1), uint8(2), uint16(3), uint32(4), uint64(5),
+ },
+ []interface{}{float32(1.5), float64(2.5)},
+ },
+ wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" +
+ "MixedFloats = [1.5, 2.5]\n",
+ },
+ "(error) slice w/ element type mismatch (one is nested array)": {
+ input: struct{ Mixed []interface{} }{
+ []interface{}{1, []interface{}{2}},
+ },
+ wantOutput: "Mixed = [1, [2]]\n",
+ },
+ "(error) slice with 1 nil element": {
+ input: struct{ NilElement1 []interface{} }{[]interface{}{nil}},
+ wantError: errArrayNilElement,
+ },
+ "(error) slice with 1 nil element (and other non-nil elements)": {
+ input: struct{ NilElement []interface{} }{
+ []interface{}{1, nil},
+ },
+ wantError: errArrayNilElement,
+ },
+ "simple map": {
+ input: map[string]int{"a": 1, "b": 2},
+ wantOutput: "a = 1\nb = 2\n",
+ },
+ "map with interface{} value type": {
+ input: map[string]interface{}{"a": 1, "b": "c"},
+ wantOutput: "a = 1\nb = \"c\"\n",
+ },
+ "map with interface{} value type, some of which are structs": {
+ input: map[string]interface{}{
+ "a": struct{ Int int }{2},
+ "b": 1,
+ },
+ wantOutput: "b = 1\n\n[a]\n Int = 2\n",
+ },
+ "nested map": {
+ input: map[string]map[string]int{
+ "a": {"b": 1},
+ "c": {"d": 2},
+ },
+ wantOutput: "[a]\n b = 1\n\n[c]\n d = 2\n",
+ },
+ "nested struct": {
+ input: struct{ Struct struct{ Int int } }{
+ struct{ Int int }{1},
+ },
+ wantOutput: "[Struct]\n Int = 1\n",
+ },
+ "nested struct and non-struct field": {
+ input: struct {
+ Struct struct{ Int int }
+ Bool bool
+ }{struct{ Int int }{1}, true},
+ wantOutput: "Bool = true\n\n[Struct]\n Int = 1\n",
+ },
+ "2 nested structs": {
+ input: struct{ Struct1, Struct2 struct{ Int int } }{
+ struct{ Int int }{1}, struct{ Int int }{2},
+ },
+ wantOutput: "[Struct1]\n Int = 1\n\n[Struct2]\n Int = 2\n",
+ },
+ "deeply nested structs": {
+ input: struct {
+ Struct1, Struct2 struct{ Struct3 *struct{ Int int } }
+ }{
+ struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}},
+ struct{ Struct3 *struct{ Int int } }{nil},
+ },
+ wantOutput: "[Struct1]\n [Struct1.Struct3]\n Int = 1" +
+ "\n\n[Struct2]\n",
+ },
+ "nested struct with nil struct elem": {
+ input: struct {
+ Struct struct{ Inner *struct{ Int int } }
+ }{
+ struct{ Inner *struct{ Int int } }{nil},
+ },
+ wantOutput: "[Struct]\n",
+ },
+ "nested struct with no fields": {
+ input: struct {
+ Struct struct{ Inner struct{} }
+ }{
+ struct{ Inner struct{} }{struct{}{}},
+ },
+ wantOutput: "[Struct]\n [Struct.Inner]\n",
+ },
+ "struct with tags": {
+ input: struct {
+ Struct struct {
+ Int int `toml:"_int"`
+ } `toml:"_struct"`
+ Bool bool `toml:"_bool"`
+ }{
+ struct {
+ Int int `toml:"_int"`
+ }{1}, true,
+ },
+ wantOutput: "_bool = true\n\n[_struct]\n _int = 1\n",
+ },
+ "embedded struct": {
+ input: struct{ Embedded }{Embedded{1}},
+ wantOutput: "_int = 1\n",
+ },
+ "embedded *struct": {
+ input: struct{ *Embedded }{&Embedded{1}},
+ wantOutput: "_int = 1\n",
+ },
+ "nested embedded struct": {
+ input: struct {
+ Struct struct{ Embedded } `toml:"_struct"`
+ }{struct{ Embedded }{Embedded{1}}},
+ wantOutput: "[_struct]\n _int = 1\n",
+ },
+ "nested embedded *struct": {
+ input: struct {
+ Struct struct{ *Embedded } `toml:"_struct"`
+ }{struct{ *Embedded }{&Embedded{1}}},
+ wantOutput: "[_struct]\n _int = 1\n",
+ },
+ "embedded non-struct": {
+ input: struct{ NonStruct }{5},
+ wantOutput: "NonStruct = 5\n",
+ },
+ "array of tables": {
+ input: struct {
+ Structs []*struct{ Int int } `toml:"struct"`
+ }{
+ []*struct{ Int int }{{1}, {3}},
+ },
+ wantOutput: "[[struct]]\n Int = 1\n\n[[struct]]\n Int = 3\n",
+ },
+ "array of tables order": {
+ input: map[string]interface{}{
+ "map": map[string]interface{}{
+ "zero": 5,
+ "arr": []map[string]int{
+ {
+ "friend": 5,
+ },
+ },
+ },
+ },
+ wantOutput: "[map]\n zero = 5\n\n [[map.arr]]\n friend = 5\n",
+ },
+ "empty key name": {
+ input: map[string]int{"": 1},
+ wantOutput: `"" = 1` + "\n",
+ },
+ "key with \\n escape": {
+ input: map[string]string{"\n": "\n"},
+ wantOutput: `"\n" = "\n"` + "\n",
+ },
+
+ "empty map name": {
+ input: map[string]interface{}{
+ "": map[string]int{"v": 1},
+ },
+ wantOutput: "[\"\"]\n v = 1\n",
+ },
+ "(error) top-level slice": {
+ input: []struct{ Int int }{{1}, {2}, {3}},
+ wantError: errNoKey,
+ },
+ "(error) map no string key": {
+ input: map[int]string{1: ""},
+ wantError: errNonString,
+ },
+
+ "tbl-in-arr-struct": {
+ input: struct {
+ Arr [][]struct{ A, B, C int }
+ }{[][]struct{ A, B, C int }{{{1, 2, 3}, {4, 5, 6}}}},
+ wantOutput: "Arr = [[{A = 1, B = 2, C = 3}, {A = 4, B = 5, C = 6}]]",
+ },
+
+ "tbl-in-arr-map": {
+ input: map[string]interface{}{
+ "arr": []interface{}{[]interface{}{
+ map[string]interface{}{
+ "a": []interface{}{"hello", "world"},
+ "b": []interface{}{1.12, 4.1},
+ "c": 1,
+ "d": map[string]interface{}{"e": "E"},
+ "f": struct{ A, B int }{1, 2},
+ "g": []struct{ A, B int }{{3, 4}, {5, 6}},
+ },
+ }},
+ },
+ wantOutput: `arr = [[{a = ["hello", "world"], b = [1.12, 4.1], c = 1, d = {e = "E"}, f = {A = 1, B = 2}, g = [{A = 3, B = 4}, {A = 5, B = 6}]}]]`,
+ },
+
+ "slice of slice": {
+ input: struct {
+ Slices [][]struct{ Int int }
+ }{
+ [][]struct{ Int int }{{{1}}, {{2}}, {{3}}},
+ },
+ wantOutput: "Slices = [[{Int = 1}], [{Int = 2}], [{Int = 3}]]",
+ },
+ }
+ for label, test := range tests {
+ encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
+ }
+}
+
+func TestEncodeDoubleTags(t *testing.T) {
+ // TODO: this needs fixing; it shouldn't emit two 'a =' keys.
+ s := struct {
+ A int `toml:"a"`
+ B int `toml:"a"`
+ C int `toml:"c"`
+ }{1, 2, 3}
+ buf := new(strings.Builder)
+ err := NewEncoder(buf).Encode(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `a = 1
+a = 2
+c = 3
+`
+ if want != buf.String() {
+ t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want)
+ }
+}
+
+type (
+ Doc1 struct{ N string }
+ Doc2 struct{ N string }
+)
+
+func (d Doc1) MarshalTOML() ([]byte, error) { return []byte(`marshal_toml = "` + d.N + `"`), nil }
+func (d Doc2) MarshalText() ([]byte, error) { return []byte(`marshal_text = "` + d.N + `"`), nil }
+
+// MarshalTOML and MarshalText on the top level type, rather than a field.
+func TestMarshalDoc(t *testing.T) {
+ t.Run("toml", func(t *testing.T) {
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(Doc1{"asd"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `marshal_toml = "asd"`
+ if want != buf.String() {
+ t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want)
+ }
+ })
+
+ t.Run("text", func(t *testing.T) {
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(Doc2{"asd"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `"marshal_text = \"asd\""`
+ if want != buf.String() {
+ t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want)
+ }
+ })
+}
+
+func encodeExpected(t *testing.T, label string, val interface{}, want string, wantErr error) {
+ t.Helper()
+ t.Run(label, func(t *testing.T) {
+ t.Helper()
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(val)
+ if err != wantErr {
+ if wantErr != nil {
+ if wantErr == errAnything && err != nil {
+ return
+ }
+ t.Errorf("want Encode error %v, got %v", wantErr, err)
+ } else {
+ t.Errorf("Encode failed: %s", err)
+ }
+ }
+ if err != nil {
+ return
+ }
+
+ have := strings.TrimSpace(buf.String())
+ want = strings.TrimSpace(want)
+ if want != have {
+ t.Errorf("\nhave:\n%s\nwant:\n%s\n",
+ "\t"+strings.ReplaceAll(have, "\n", "\n\t"),
+ "\t"+strings.ReplaceAll(want, "\n", "\n\t"))
+ }
+ })
+}
diff --git a/error.go b/error.go
new file mode 100644
index 0000000..efd6886
--- /dev/null
+++ b/error.go
@@ -0,0 +1,279 @@
+package toml
+
+import (
+ "fmt"
+ "strings"
+)
+
+// ParseError is returned when there is an error parsing the TOML syntax such as
+// invalid syntax, duplicate keys, etc.
+//
+// In addition to the error message itself, you can also print detailed location
+// information with context by using [ErrorWithPosition]:
+//
+// toml: error: Key 'fruit' was already created and cannot be used as an array.
+//
+// At line 4, column 2-7:
+//
+// 2 | fruit = []
+// 3 |
+// 4 | [[fruit]] # Not allowed
+// ^^^^^
+//
+// [ErrorWithUsage] can be used to print the above with some more detailed usage
+// guidance:
+//
+// toml: error: newlines not allowed within inline tables
+//
+// At line 1, column 18:
+//
+// 1 | x = [{ key = 42 #
+// ^
+//
+// Error help:
+//
+// Inline tables must always be on a single line:
+//
+// table = {key = 42, second = 43}
+//
+// It is invalid to split them over multiple lines like so:
+//
+// # INVALID
+// table = {
+// key = 42,
+// second = 43
+// }
+//
+// Use regular for this:
+//
+// [table]
+// key = 42
+// second = 43
+type ParseError struct {
+ Message string // Short technical message.
+ Usage string // Longer message with usage guidance; may be blank.
+ Position Position // Position of the error
+ LastKey string // Last parsed key, may be blank.
+
+ // Line the error occurred.
+ //
+ // Deprecated: use [Position].
+ Line int
+
+ err error
+ input string
+}
+
+// Position of an error.
+type Position struct {
+ Line int // Line number, starting at 1.
+ Start int // Start of error, as byte offset starting at 0.
+ Len int // Lenght in bytes.
+}
+
+func (pe ParseError) Error() string {
+ msg := pe.Message
+ if msg == "" { // Error from errorf()
+ msg = pe.err.Error()
+ }
+
+ if pe.LastKey == "" {
+ return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
+ }
+ return fmt.Sprintf("toml: line %d (last key %q): %s",
+ pe.Position.Line, pe.LastKey, msg)
+}
+
+// ErrorWithPosition returns the error with detailed location context.
+//
+// See the documentation on [ParseError].
+func (pe ParseError) ErrorWithPosition() string {
+ if pe.input == "" { // Should never happen, but just in case.
+ return pe.Error()
+ }
+
+ var (
+ lines = strings.Split(pe.input, "\n")
+ col = pe.column(lines)
+ b = new(strings.Builder)
+ )
+
+ msg := pe.Message
+ if msg == "" {
+ msg = pe.err.Error()
+ }
+
+ // TODO: don't show control characters as literals? This may not show up
+ // well everywhere.
+
+ if pe.Position.Len == 1 {
+ fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
+ msg, pe.Position.Line, col+1)
+ } else {
+ fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
+ msg, pe.Position.Line, col, col+pe.Position.Len)
+ }
+ if pe.Position.Line > 2 {
+ fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3])
+ }
+ if pe.Position.Line > 1 {
+ fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2])
+ }
+ fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
+ fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len))
+ return b.String()
+}
+
+// ErrorWithUsage returns the error with detailed location context and usage
+// guidance.
+//
+// See the documentation on [ParseError].
+func (pe ParseError) ErrorWithUsage() string {
+ m := pe.ErrorWithPosition()
+ if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
+ lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
+ for i := range lines {
+ if lines[i] != "" {
+ lines[i] = " " + lines[i]
+ }
+ }
+ return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
+ }
+ return m
+}
+
+func (pe ParseError) column(lines []string) int {
+ var pos, col int
+ for i := range lines {
+ ll := len(lines[i]) + 1 // +1 for the removed newline
+ if pos+ll >= pe.Position.Start {
+ col = pe.Position.Start - pos
+ if col < 0 { // Should never happen, but just in case.
+ col = 0
+ }
+ break
+ }
+ pos += ll
+ }
+
+ return col
+}
+
+type (
+ errLexControl struct{ r rune }
+ errLexEscape struct{ r rune }
+ errLexUTF8 struct{ b byte }
+ errLexInvalidNum struct{ v string }
+ errLexInvalidDate struct{ v string }
+ errLexInlineTableNL struct{}
+ errLexStringNL struct{}
+ errParseRange struct {
+ i interface{} // int or float
+ size string // "int64", "uint16", etc.
+ }
+ errParseDuration struct{ d string }
+)
+
+func (e errLexControl) Error() string {
+ return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
+}
+func (e errLexControl) Usage() string { return "" }
+
+func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
+func (e errLexEscape) Usage() string { return usageEscape }
+func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
+func (e errLexUTF8) Usage() string { return "" }
+func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) }
+func (e errLexInvalidNum) Usage() string { return "" }
+func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
+func (e errLexInvalidDate) Usage() string { return "" }
+func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
+func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
+func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
+func (e errLexStringNL) Usage() string { return usageStringNewline }
+func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
+func (e errParseRange) Usage() string { return usageIntOverflow }
+func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
+func (e errParseDuration) Usage() string { return usageDuration }
+
+const usageEscape = `
+A '\' inside a "-delimited string is interpreted as an escape character.
+
+The following escape sequences are supported:
+\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
+
+To prevent a '\' from being recognized as an escape character, use either:
+
+- a ' or '''-delimited string; escape characters aren't processed in them; or
+- write two backslashes to get a single backslash: '\\'.
+
+If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
+instead of '\' will usually also work: "C:/Users/martin".
+`
+
+const usageInlineNewline = `
+Inline tables must always be on a single line:
+
+ table = {key = 42, second = 43}
+
+It is invalid to split them over multiple lines like so:
+
+ # INVALID
+ table = {
+ key = 42,
+ second = 43
+ }
+
+Use regular for this:
+
+ [table]
+ key = 42
+ second = 43
+`
+
+const usageStringNewline = `
+Strings must always be on a single line, and cannot span more than one line:
+
+ # INVALID
+ string = "Hello,
+ world!"
+
+Instead use """ or ''' to split strings over multiple lines:
+
+ string = """Hello,
+ world!"""
+`
+
+const usageIntOverflow = `
+This number is too large; this may be an error in the TOML, but it can also be a
+bug in the program that uses too small of an integer.
+
+The maximum and minimum values are:
+
+ size │ lowest │ highest
+ ───────┼────────────────┼──────────
+ int8 │ -128 │ 127
+ int16 │ -32,768 │ 32,767
+ int32 │ -2,147,483,648 │ 2,147,483,647
+ int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
+ uint8 │ 0 │ 255
+ uint16 │ 0 │ 65535
+ uint32 │ 0 │ 4294967295
+ uint64 │ 0 │ 1.8 × 10¹⁸
+
+int refers to int32 on 32-bit systems and int64 on 64-bit systems.
+`
+
+const usageDuration = `
+A duration must be as "number<unit>", without any spaces. Valid units are:
+
+ ns nanoseconds (billionth of a second)
+ us, µs microseconds (millionth of a second)
+ ms milliseconds (thousands of a second)
+ s seconds
+ m minutes
+ h hours
+
+You can combine multiple units; for example "5m10s" for 5 minutes and 10
+seconds.
+`
diff --git a/error_test.go b/error_test.go
new file mode 100644
index 0000000..cd95263
--- /dev/null
+++ b/error_test.go
@@ -0,0 +1,245 @@
+//go:build go1.16
+// +build go1.16
+
+package toml_test
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "math"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/BurntSushi/toml"
+ tomltest "github.com/BurntSushi/toml/internal/toml-test"
+)
+
+func TestErrorPosition(t *testing.T) {
+ // Note: take care to use leading spaces (not tabs).
+ tests := []struct {
+ test, err string
+ }{
+ {"array/missing-separator.toml", `
+toml: error: expected a comma (',') or array terminator (']'), but got '2'
+
+At line 1, column 13:
+
+ 1 | wrong = [ 1 2 3 ]
+ ^`},
+
+ {"array/no-close-2.toml", `
+toml: error: expected a comma (',') or array terminator (']'), but got end of file
+
+At line 1, column 10:
+
+ 1 | x = [42 #
+ ^`},
+
+ {"array/tables-2.toml", `
+toml: error: Key 'fruit.variety' has already been defined.
+
+At line 9, column 3-8:
+
+ 7 |
+ 8 | # This table conflicts with the previous table
+ 9 | [fruit.variety]
+ ^^^^^`},
+ {"datetime/trailing-t.toml", `
+toml: error: Invalid TOML Datetime: "2006-01-30T".
+
+At line 2, column 4-15:
+
+ 1 | # Date cannot end with trailing T
+ 2 | d = 2006-01-30T
+ ^^^^^^^^^^^`},
+ }
+
+ fsys := tomltest.EmbeddedTests()
+ for _, tt := range tests {
+ t.Run(tt.test, func(t *testing.T) {
+ input, err := fs.ReadFile(fsys, "invalid/"+tt.test)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var x interface{}
+ _, err = toml.Decode(string(input), &x)
+ if err == nil {
+ t.Fatal("err is nil")
+ }
+
+ var pErr toml.ParseError
+ if !errors.As(err, &pErr) {
+ t.Errorf("err is not a ParseError: %T %[1]v", err)
+ }
+
+ tt.err = tt.err[1:] + "\n" // Remove first newline, and add trailing.
+ want := pErr.ErrorWithUsage()
+
+ if !strings.Contains(want, tt.err) {
+ t.Fatalf("\nwant:\n%s\nhave:\n%s", tt.err, want)
+ }
+ })
+ }
+}
+
+func TestParseError(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ toml, err string
+ }{
+ {
+ &struct{ Int int8 }{},
+ "Int = 200",
+ `| toml: error: 200 is out of range for int8
+ |
+ | At line 1, column 6-9:
+ |
+ | 1 | Int = 200
+ | ^^^
+ | Error help:
+ |
+ | This number is too large; this may be an error in the TOML, but it can also be a
+ | bug in the program that uses too small of an integer.
+ |
+ | The maximum and minimum values are:
+ |
+ | size │ lowest │ highest
+ | ───────┼────────────────┼──────────
+ | int8 │ -128 │ 127
+ | int16 │ -32,768 │ 32,767
+ | int32 │ -2,147,483,648 │ 2,147,483,647
+ | int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
+ | uint8 │ 0 │ 255
+ | uint16 │ 0 │ 65535
+ | uint32 │ 0 │ 4294967295
+ | uint64 │ 0 │ 1.8 × 10¹⁸
+ |
+ | int refers to int32 on 32-bit systems and int64 on 64-bit systems.
+ `,
+ },
+ {
+ &struct{ Int int }{},
+ fmt.Sprintf("Int = %d", uint64(math.MaxInt64+1)),
+ `| toml: error: 9223372036854775808 is out of range for int64
+ |
+ | At line 1, column 6-25:
+ |
+ | 1 | Int = 9223372036854775808
+ | ^^^^^^^^^^^^^^^^^^^
+ | Error help:
+ |
+ | This number is too large; this may be an error in the TOML, but it can also be a
+ | bug in the program that uses too small of an integer.
+ |
+ | The maximum and minimum values are:
+ |
+ | size │ lowest │ highest
+ | ───────┼────────────────┼──────────
+ | int8 │ -128 │ 127
+ | int16 │ -32,768 │ 32,767
+ | int32 │ -2,147,483,648 │ 2,147,483,647
+ | int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
+ | uint8 │ 0 │ 255
+ | uint16 │ 0 │ 65535
+ | uint32 │ 0 │ 4294967295
+ | uint64 │ 0 │ 1.8 × 10¹⁸
+ |
+ | int refers to int32 on 32-bit systems and int64 on 64-bit systems.
+ `,
+ },
+ {
+ &struct{ Float float32 }{},
+ "Float = 1.1e99",
+ `
+ | toml: error: 1.1e+99 is out of range for float32
+ |
+ | At line 1, column 8-14:
+ |
+ | 1 | Float = 1.1e99
+ | ^^^^^^
+ | Error help:
+ |
+ | This number is too large; this may be an error in the TOML, but it can also be a
+ | bug in the program that uses too small of an integer.
+ |
+ | The maximum and minimum values are:
+ |
+ | size │ lowest │ highest
+ | ───────┼────────────────┼──────────
+ | int8 │ -128 │ 127
+ | int16 │ -32,768 │ 32,767
+ | int32 │ -2,147,483,648 │ 2,147,483,647
+ | int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
+ | uint8 │ 0 │ 255
+ | uint16 │ 0 │ 65535
+ | uint32 │ 0 │ 4294967295
+ | uint64 │ 0 │ 1.8 × 10¹⁸
+ |
+ | int refers to int32 on 32-bit systems and int64 on 64-bit systems.
+ `,
+ },
+
+ {
+ &struct{ D time.Duration }{},
+ `D = "99 bottles"`,
+ `
+ | toml: error: invalid duration: "99 bottles"
+ |
+ | At line 1, column 5-15:
+ |
+ | 1 | D = "99 bottles"
+ | ^^^^^^^^^^
+ | Error help:
+ |
+ | A duration must be as "number<unit>", without any spaces. Valid units are:
+ |
+ | ns nanoseconds (billionth of a second)
+ | us, µs microseconds (millionth of a second)
+ | ms milliseconds (thousands of a second)
+ | s seconds
+ | m minutes
+ | h hours
+ |
+ | You can combine multiple units; for example "5m10s" for 5 minutes and 10
+ | seconds.
+ `,
+ },
+ }
+
+ prep := func(s string) string {
+ lines := strings.Split(strings.TrimSpace(s), "\n")
+ for i := range lines {
+ if j := strings.IndexByte(lines[i], '|'); j >= 0 {
+ lines[i] = lines[i][j+1:]
+ lines[i] = strings.Replace(lines[i], " ", "", 1)
+ }
+ }
+ return strings.Join(lines, "\n") + "\n"
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := toml.Decode(tt.toml, tt.in)
+ if err == nil {
+ t.Fatalf("err is nil; decoded: %#v", tt.in)
+ }
+
+ var pErr toml.ParseError
+ if !errors.As(err, &pErr) {
+ t.Fatalf("err is not a ParseError: %#v", err)
+ }
+
+ tt.err = prep(tt.err)
+ have := pErr.ErrorWithUsage()
+
+ // have = strings.ReplaceAll(have, " ", "·")
+ // tt.err = strings.ReplaceAll(tt.err, " ", "·")
+ if have != tt.err {
+ t.Fatalf("\nwant:\n%s\nhave:\n%s", tt.err, have)
+ }
+ })
+ }
+}
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..eb51b4a
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,387 @@
+package toml_test
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "net/mail"
+ "time"
+
+ "github.com/BurntSushi/toml"
+)
+
+func ExampleEncoder_Encode() {
+ var (
+ date, _ = time.Parse(time.RFC822, "14 Mar 10 18:00 UTC")
+ buf = new(bytes.Buffer)
+ )
+ err := toml.NewEncoder(buf).Encode(map[string]interface{}{
+ "date": date,
+ "counts": []int{1, 1, 2, 3, 5, 8},
+ "hash": map[string]string{
+ "key1": "val1",
+ "key2": "val2",
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println(buf.String())
+
+ // Output:
+ // counts = [1, 1, 2, 3, 5, 8]
+ // date = 2010-03-14T18:00:00Z
+ //
+ // [hash]
+ // key1 = "val1"
+ // key2 = "val2"
+}
+
+func ExampleMetaData_PrimitiveDecode() {
+ tomlBlob := `
+ ranking = ["Springsteen", "J Geils"]
+
+ [bands.Springsteen]
+ started = 1973
+ albums = ["Greetings", "WIESS", "Born to Run", "Darkness"]
+
+ [bands."J Geils"]
+ started = 1970
+ albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"]
+ `
+
+ type (
+ band struct {
+ Started int
+ Albums []string
+ }
+ classics struct {
+ Ranking []string
+ Bands map[string]toml.Primitive
+ }
+ )
+
+ // Do the initial decode; reflection is delayed on Primitive values.
+ var music classics
+ md, err := toml.Decode(tomlBlob, &music)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // MetaData still includes information on Primitive values.
+ fmt.Printf("Is `bands.Springsteen` defined? %v\n",
+ md.IsDefined("bands", "Springsteen"))
+
+ // Decode primitive data into Go values.
+ for _, artist := range music.Ranking {
+ // A band is a primitive value, so we need to decode it to get a real
+ // `band` value.
+ primValue := music.Bands[artist]
+
+ var aBand band
+ err = md.PrimitiveDecode(primValue, &aBand)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s started in %d.\n", artist, aBand.Started)
+ }
+
+ // Check to see if there were any fields left undecoded. Note that this
+ // won't be empty before decoding the Primitive value!
+ fmt.Printf("Undecoded: %q\n", md.Undecoded())
+
+ // Output:
+ // Is `bands.Springsteen` defined? true
+ // Springsteen started in 1973.
+ // J Geils started in 1970.
+ // Undecoded: []
+}
+
+func ExampleDecode() {
+ tomlBlob := `
+ # Some comments.
+ [alpha]
+ ip = "10.0.0.1"
+
+ [alpha.config]
+ Ports = [ 8001, 8002 ]
+ Location = "Toronto"
+ Created = 1987-07-05T05:45:00Z
+
+ [beta]
+ ip = "10.0.0.2"
+
+ [beta.config]
+ Ports = [ 9001, 9002 ]
+ Location = "New Jersey"
+ Created = 1887-01-05T05:55:00Z
+ `
+
+ type (
+ serverConfig struct {
+ Ports []int
+ Location string
+ Created time.Time
+ }
+ server struct {
+ IP string `toml:"ip,omitempty"`
+ Config serverConfig `toml:"config"`
+ }
+ servers map[string]server
+ )
+
+ var config servers
+ _, err := toml.Decode(tomlBlob, &config)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, name := range []string{"alpha", "beta"} {
+ s := config[name]
+ fmt.Printf("Server: %s (ip: %s) in %s created on %s\n",
+ name, s.IP, s.Config.Location,
+ s.Config.Created.Format("2006-01-02"))
+ fmt.Printf("Ports: %v\n", s.Config.Ports)
+ }
+
+ // Output:
+ // Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05
+ // Ports: [8001 8002]
+ // Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05
+ // Ports: [9001 9002]
+}
+
+type address struct{ *mail.Address }
+
+func (a *address) UnmarshalText(text []byte) error {
+ var err error
+ a.Address, err = mail.ParseAddress(string(text))
+ return err
+}
+
+// Example Unmarshaler shows how to decode TOML strings into your own
+// custom data type.
+func Example_unmarshaler() {
+ blob := `
+ contacts = [
+ "Donald Duck <donald@duckburg.com>",
+ "Scrooge McDuck <scrooge@duckburg.com>",
+ ]
+ `
+
+ var contacts struct {
+ // Implementation of the address type:
+ //
+ // type address struct{ *mail.Address }
+ //
+ // func (a *address) UnmarshalText(text []byte) error {
+ // var err error
+ // a.Address, err = mail.ParseAddress(string(text))
+ // return err
+ // }
+
+ Contacts []address
+ }
+
+ _, err := toml.Decode(blob, &contacts)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, c := range contacts.Contacts {
+ fmt.Printf("%#v\n", c.Address)
+ }
+
+ // Output:
+ // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
+ // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
+}
+
+// Example StrictDecoding shows how to detect if there are keys in the TOML
+// document that weren't decoded into the value given. This is useful for
+// returning an error to the user if they've included extraneous fields in their
+// configuration.
+func Example_strictDecoding() {
+ var blob = `
+ key1 = "value1"
+ key2 = "value2"
+ key3 = "value3"
+ `
+
+ var conf struct {
+ Key1 string
+ Key3 string
+ }
+ md, err := toml.Decode(blob, &conf)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Undecoded keys: %q\n", md.Undecoded())
+ // Output:
+ // Undecoded keys: ["key2"]
+}
+
+type order struct {
+ // NOTE `order.parts` is a private slice of type `part` which is an
+ // interface and may only be loaded from toml using the UnmarshalTOML()
+ // method of the Umarshaler interface.
+ parts parts
+}
+
+type parts []part
+
+type part interface {
+ Name() string
+}
+
+type valve struct {
+ Type string
+ ID string
+ Size float32
+ Rating int
+}
+
+func (v *valve) Name() string {
+ return fmt.Sprintf("VALVE: %s", v.ID)
+}
+
+type pipe struct {
+ Type string
+ ID string
+ Length float32
+ Diameter int
+}
+
+func (p *pipe) Name() string {
+ return fmt.Sprintf("PIPE: %s", p.ID)
+}
+
+type cable struct {
+ Type string
+ ID string
+ Length int
+ Rating float32
+}
+
+func (c *cable) Name() string {
+ return fmt.Sprintf("CABLE: %s", c.ID)
+}
+
+func (o *order) UnmarshalTOML(data interface{}) error {
+ // NOTE the example below contains detailed type casting to show how the
+ // 'data' is retrieved. In operational use, a type cast wrapper may be
+ // preferred e.g.
+ //
+ // func AsMap(v interface{}) (map[string]interface{}, error) {
+ // return v.(map[string]interface{})
+ // }
+ //
+ // resulting in:
+ // d, _ := AsMap(data)
+ //
+
+ d, _ := data.(map[string]interface{})
+ parts, _ := d["parts"].([]map[string]interface{})
+
+ for _, p := range parts {
+
+ typ, _ := p["type"].(string)
+ id, _ := p["id"].(string)
+
+ // detect the type of part and handle each case
+ switch p["type"] {
+ case "valve":
+
+ size := float32(p["size"].(float64))
+ rating := int(p["rating"].(int64))
+
+ valve := &valve{
+ Type: typ,
+ ID: id,
+ Size: size,
+ Rating: rating,
+ }
+
+ o.parts = append(o.parts, valve)
+
+ case "pipe":
+
+ length := float32(p["length"].(float64))
+ diameter := int(p["diameter"].(int64))
+
+ pipe := &pipe{
+ Type: typ,
+ ID: id,
+ Length: length,
+ Diameter: diameter,
+ }
+
+ o.parts = append(o.parts, pipe)
+
+ case "cable":
+
+ length := int(p["length"].(int64))
+ rating := float32(p["rating"].(float64))
+
+ cable := &cable{
+ Type: typ,
+ ID: id,
+ Length: length,
+ Rating: rating,
+ }
+
+ o.parts = append(o.parts, cable)
+
+ }
+ }
+
+ return nil
+}
+
+// Example UnmarshalTOML shows how to implement a struct type that knows how to
+// unmarshal itself. The struct must take full responsibility for mapping the
+// values passed into the struct. The method may be used with interfaces in a
+// struct in cases where the actual type is not known until the data is
+// examined.
+func Example_unmarshalTOML() {
+ blob := `
+ [[parts]]
+ type = "valve"
+ id = "valve-1"
+ size = 1.2
+ rating = 4
+
+ [[parts]]
+ type = "valve"
+ id = "valve-2"
+ size = 2.1
+ rating = 5
+
+ [[parts]]
+ type = "pipe"
+ id = "pipe-1"
+ length = 2.1
+ diameter = 12
+
+ [[parts]]
+ type = "cable"
+ id = "cable-1"
+ length = 12
+ rating = 3.1
+ `
+
+ // See example_test.go in the source for the implementation of the order
+ // type.
+ o := &order{}
+
+ err := toml.Unmarshal([]byte(blob), o)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println(len(o.parts))
+ for _, part := range o.parts {
+ fmt.Println(part.Name())
+ }
+}
diff --git a/fuzz_test.go b/fuzz_test.go
new file mode 100644
index 0000000..c0162eb
--- /dev/null
+++ b/fuzz_test.go
@@ -0,0 +1,81 @@
+//go:build go1.18
+// +build go1.18
+
+package toml
+
+import (
+ "bytes"
+ "testing"
+)
+
+func FuzzDecode(f *testing.F) {
+ buf := make([]byte, 0, 2048)
+
+ f.Add(`
+# This is an example TOML document which shows most of its features.
+
+# Simple key/value with a string.
+title = "TOML example \U0001F60A"
+
+desc = """
+An example TOML document. \
+"""
+
+# Array with integers and floats in the various allowed formats.
+integers = [42, 0x42, 0o42, 0b0110]
+floats = [1.42, 1e-02]
+
+# Array with supported datetime formats.
+times = [
+ 2021-11-09T15:16:17+01:00, # datetime with timezone.
+ 2021-11-09T15:16:17Z, # UTC datetime.
+ 2021-11-09T15:16:17, # local datetime.
+ 2021-11-09, # local date.
+ 15:16:17, # local time.
+]
+
+# Durations.
+duration = ["4m49s", "8m03s", "1231h15m55s"]
+
+# Table with inline tables.
+distros = [
+ {name = "Arch Linux", packages = "pacman"},
+ {name = "Void Linux", packages = "xbps"},
+ {name = "Debian", packages = "apt"},
+]
+
+# Create new table; note the "servers" table is created implicitly.
+[servers.alpha]
+ # You can indent as you please, tabs or spaces.
+ ip = '10.0.0.1'
+ hostname = 'server1'
+ enabled = false
+[servers.beta]
+ ip = '10.0.0.2'
+ hostname = 'server2'
+ enabled = true
+
+# Start a new table array; note that the "characters" table is created implicitly.
+[[characters.star-trek]]
+ name = "James Kirk"
+ rank = "Captain\u0012 \t"
+[[characters.star-trek]]
+ name = "Spock"
+ rank = "Science officer"
+
+[undecoded] # To show the MetaData.Undecoded() feature.
+ key = "This table intentionally left undecoded"
+`)
+ f.Fuzz(func(t *testing.T, file string) {
+ var m map[string]interface{}
+ _, err := Decode(file, &m)
+ if err != nil {
+ t.Skip()
+ }
+
+ NewEncoder(bytes.NewBuffer(buf)).Encode(m)
+
+ // TODO: should check if the output is equal to the input, too, but some
+ // information is lost when encoding.
+ })
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..8298948
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/BurntSushi/toml
+
+go 1.16
diff --git a/internal/tag/add.go b/internal/tag/add.go
new file mode 100644
index 0000000..88f6950
--- /dev/null
+++ b/internal/tag/add.go
@@ -0,0 +1,76 @@
+package tag
+
+import (
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+// Add JSON tags to a data structure as expected by toml-test.
+func Add(key string, tomlData interface{}) interface{} {
+ // Switch on the data type.
+ switch orig := tomlData.(type) {
+ default:
+ panic(fmt.Sprintf("Unknown type: %T", tomlData))
+
+ // A table: we don't need to add any tags, just recurse for every table
+ // entry.
+ case map[string]interface{}:
+ typed := make(map[string]interface{}, len(orig))
+ for k, v := range orig {
+ typed[k] = Add(k, v)
+ }
+ return typed
+
+ // An array: we don't need to add any tags, just recurse for every table
+ // entry.
+ case []map[string]interface{}:
+ typed := make([]map[string]interface{}, len(orig))
+ for i, v := range orig {
+ typed[i] = Add("", v).(map[string]interface{})
+ }
+ return typed
+ case []interface{}:
+ typed := make([]interface{}, len(orig))
+ for i, v := range orig {
+ typed[i] = Add("", v)
+ }
+ return typed
+
+ // Datetime: tag as datetime.
+ case time.Time:
+ switch orig.Location() {
+ default:
+ return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
+ case internal.LocalDatetime:
+ return tag("datetime-local", orig.Format("2006-01-02T15:04:05.999999999"))
+ case internal.LocalDate:
+ return tag("date-local", orig.Format("2006-01-02"))
+ case internal.LocalTime:
+ return tag("time-local", orig.Format("15:04:05.999999999"))
+ }
+
+ // Tag primitive values: bool, string, int, and float64.
+ case bool:
+ return tag("bool", fmt.Sprintf("%v", orig))
+ case string:
+ return tag("string", orig)
+ case int64:
+ return tag("integer", fmt.Sprintf("%d", orig))
+ case float64:
+ // Special case for nan since NaN == NaN is false.
+ if math.IsNaN(orig) {
+ return tag("float", "nan")
+ }
+ return tag("float", fmt.Sprintf("%v", orig))
+ }
+}
+
+func tag(typeName string, data interface{}) map[string]interface{} {
+ return map[string]interface{}{
+ "type": typeName,
+ "value": data,
+ }
+}
diff --git a/internal/tag/rm.go b/internal/tag/rm.go
new file mode 100644
index 0000000..2647e64
--- /dev/null
+++ b/internal/tag/rm.go
@@ -0,0 +1,111 @@
+package tag
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+// Remove JSON tags to a data structure as returned by toml-test.
+func Remove(typedJson interface{}) (interface{}, error) {
+ // Switch on the data type.
+ switch v := typedJson.(type) {
+
+ // Object: this can either be a TOML table or a primitive with tags.
+ case map[string]interface{}:
+ // This value represents a primitive: remove the tags and return just
+ // the primitive value.
+ if len(v) == 2 && in("type", v) && in("value", v) {
+ ut, err := untag(v)
+ if err != nil {
+ return ut, fmt.Errorf("tag.Remove: %w", err)
+ }
+ return ut, nil
+ }
+
+ // Table: remove tags on all children.
+ m := make(map[string]interface{}, len(v))
+ for k, v2 := range v {
+ var err error
+ m[k], err = Remove(v2)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return m, nil
+
+ // Array: remove tags from all items.
+ case []interface{}:
+ a := make([]interface{}, len(v))
+ for i := range v {
+ var err error
+ a[i], err = Remove(v[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+ return a, nil
+ }
+
+ // The top level must be an object or array.
+ return nil, fmt.Errorf("tag.Remove: unrecognized JSON format '%T'", typedJson)
+}
+
+// Check if key is in the table m.
+func in(key string, m map[string]interface{}) bool {
+ _, ok := m[key]
+ return ok
+}
+
+// Return a primitive: read the "type" and convert the "value" to that.
+func untag(typed map[string]interface{}) (interface{}, error) {
+ t := typed["type"].(string)
+ v := typed["value"].(string)
+ switch t {
+ case "string":
+ return v, nil
+ case "integer":
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("untag: %w", err)
+ }
+ return n, nil
+ case "float":
+ f, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return nil, fmt.Errorf("untag: %w", err)
+ }
+ return f, nil
+ case "datetime":
+ return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", nil)
+ case "datetime-local":
+ return parseTime(v, "2006-01-02T15:04:05.999999999", internal.LocalDatetime)
+ case "date-local":
+ return parseTime(v, "2006-01-02", internal.LocalDate)
+ case "time-local":
+ return parseTime(v, "15:04:05.999999999", internal.LocalTime)
+ case "bool":
+ switch v {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ }
+ return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
+ }
+
+ return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
+}
+
+func parseTime(v, format string, l *time.Location) (time.Time, error) {
+ t, err := time.Parse(format, v)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("could not parse %q as a datetime: %w", v, err)
+ }
+ if l != nil {
+ t = t.In(l)
+ }
+ return t, nil
+}
diff --git a/internal/toml-test/.gogo-release b/internal/toml-test/.gogo-release
new file mode 100644
index 0000000..8d74de3
--- /dev/null
+++ b/internal/toml-test/.gogo-release
@@ -0,0 +1 @@
+build_flags="-trimpath -ldflags '-w -s -X \"zgo.at/zli.version=$tag$commit_info\" -X \"zgo.at/zli.progname=toml-test\"' ./cmd/toml-test"
diff --git a/internal/toml-test/COPYING b/internal/toml-test/COPYING
new file mode 100644
index 0000000..93b2202
--- /dev/null
+++ b/internal/toml-test/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 TOML authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/internal/toml-test/README.md b/internal/toml-test/README.md
new file mode 100644
index 0000000..398b1c9
--- /dev/null
+++ b/internal/toml-test/README.md
@@ -0,0 +1,271 @@
+`toml-test` is a language-agnostic test suite to verify the correctness of
+[TOML][t] parsers and writers.
+
+Tests are divided into two groups: "invalid" and "valid". Decoders or encoders
+that reject "invalid" tests pass the tests, and decoders that accept "valid"
+tests and output precisely what is expected pass the tests. The output format is
+JSON, described below.
+
+Both encoders and decoders share valid tests, except an encoder accepts JSON and
+outputs TOML rather than the reverse. The TOML representations are read with a
+blessed decoder and is compared. Encoders have their own set of invalid tests in
+the invalid-encoder directory. The JSON given to a TOML encoder is in the same
+format as the JSON that a TOML decoder should output.
+
+Compatible with TOML version [v1.0.0][v1].
+
+[t]: https://toml.io
+[v1]: https://toml.io/en/v1.0.0
+
+Installation
+------------
+There are binaries on the [release page][r]; these are statically compiled and
+should run in most environments. It's recommended you use a binary, or a tagged
+release if you build from source especially in CI environments. This prevents
+your tests from breaking on changes to tests in this tool.
+
+To compile from source you will need Go 1.16 or newer (older versions will *not*
+work):
+
+ $ git clone https://github.com/BurntSushi/toml-test.git
+ $ cd toml-test
+ $ go build ./cmd/toml-test
+
+This will build a `./toml-test` binary.
+
+[r]: https://github.com/BurntSushi/toml-test/releases
+
+Usage
+-----
+`toml-test` accepts an encoder or decoder as the first positional argument, for
+example:
+
+ $ toml-test my-toml-decoder
+ $ toml-test my-toml-encoder -encoder
+
+The `-encoder` flag is used to signal that this is an encoder rather than a
+decoder.
+
+For example, to run the tests against the Go TOML library:
+
+ # Install my parser
+ $ go install github.com/BurntSushi/toml/cmd/toml-test-decoder@master
+ $ go install github.com/BurntSushi/toml/cmd/toml-test-encoder@master
+
+ $ toml-test toml-test-decoder
+ toml-test [toml-test-decoder]: using embeded tests: 278 passed
+
+ $ toml-test -encoder toml-test-encoder
+ toml-test [toml-test-encoder]: using embeded tests: 94 passed, 0 failed
+
+The default is to use the tests compiled in the binary; you can use `-testdir`
+to load tests from the filesystem. You can use `-run [name]` or `-skip [name]`
+to run or skip specific tests. Both flags can be given more than once and accept
+glob patterns: `-run 'valid/string/*'`.
+
+See `toml-test -help` for detailed usage.
+
+### Implementing a decoder
+For your decoder to be compatible with `toml-test` it **must** satisfy the
+expected interface:
+
+- Your decoder **must** accept TOML data on `stdin` until EOF.
+- If the TOML data is invalid, your decoder **must** return with a non-zero
+ exit, code indicating an error.
+- If the TOML data is valid, your decoder **must** output a JSON encoding of
+ that data on `stdout` and return with a zero exit code indicating success.
+
+An example in pseudocode:
+
+ toml_data = read_stdin()
+
+ parsed_toml = decode_toml(toml_data)
+
+ if error_parsing_toml():
+ print_error_to_stderr()
+ exit(1)
+
+ print_as_tagged_json(parsed_toml)
+ exit(0)
+
+Details on the tagged JSON is explained below in "JSON encoding".
+
+### Implementing an encoder
+For your encoder to be compatible with `toml-test`, it **must** satisfy the
+expected interface:
+
+- Your encoder **must** accept JSON data on `stdin` until EOF.
+- If the JSON data cannot be converted to a valid TOML representation, your
+ encoder **must** return with a non-zero exit code indicating an error.
+- If the JSON data can be converted to a valid TOML representation, your encoder
+ **must** output a TOML encoding of that data on `stdout` and return with a
+ zero exit code indicating success.
+
+An example in pseudocode:
+
+ json_data = read_stdin()
+
+ parsed_json_with_tags = decode_json(json_data)
+
+ if error_parsing_json():
+ print_error_to_stderr()
+ exit(1)
+
+ print_as_toml(parsed_json_with_tags)
+ exit(0)
+
+JSON encoding
+-------------
+The following JSON encoding applies equally to both encoders and decoders:
+
+- TOML tables correspond to JSON objects.
+- TOML table arrays correspond to JSON arrays.
+- TOML values correspond to a special JSON object of the form:
+ `{"type": "{TTYPE}", "value": {TVALUE}}`
+
+In the above, `TTYPE` may be one of:
+
+- string
+- integer
+- float
+- bool
+- datetime
+- datetime-local
+- date-local
+- time-local
+
+`TVALUE` is always a JSON string.
+
+Empty hashes correspond to empty JSON objects (`{}`) and empty arrays correspond
+to empty JSON arrays (`[]`).
+
+Offset datetimes should be encoded in RFC 3339; Local datetimes should be
+encoded following RFC 3339 without the offset part. Local dates should be
+encoded as the date part of RFC 3339 and Local times as the time part.
+
+Examples:
+
+ TOML JSON
+
+ a = 42 {"type": "integer": "value": "42}
+
+---
+
+ [tbl] {"tbl": {
+ a = 42 "a": {"type": "integer": "value": "42}
+ }}
+
+---
+
+ a = ["a", 2] {"a": [
+ {"type": "string", "value": "1"},
+ {"type: "integer": "value": "2"}
+ ]}
+
+Or a more complex example:
+
+```toml
+best-day-ever = 1987-07-05T17:45:00Z
+
+[numtheory]
+boring = false
+perfection = [6, 28, 496]
+```
+
+And the JSON encoding expected by `toml-test` is:
+
+```json
+{
+ "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"},
+ "numtheory": {
+ "boring": {"type": "bool", "value": "false"},
+ "perfection": [
+ {"type": "integer", "value": "6"},
+ {"type": "integer", "value": "28"},
+ {"type": "integer", "value": "496"}
+ ]
+ }
+}
+```
+
+Note that the only JSON values ever used are objects, arrays and strings.
+
+An example implementation can be found in the BurnSushi/toml:
+
+- [Add tags](https://github.com/BurntSushi/toml/blob/master/internal/tag/add.go)
+- [Remove tags](https://github.com/BurntSushi/toml/blob/master/internal/tag/rm.go)
+
+Implementation-defined behaviour
+--------------------------------
+This only tests behaviour that's should be true for every encoder implementing
+TOML; a few things are left up to implementations, and are not tested here.
+
+- Millisecond precision (4 digits) is required for datetimes and times, and
+ further precision is implementation-specific, and any greater precision than
+ is supported must be truncated (not rounded).
+
+ This tests only millisecond precision, and not any further precision or the
+ truncation of it.
+
+
+Assumptions of Truth
+--------------------
+The following are taken as ground truths by `toml-test`:
+
+- All tests classified as `invalid` **are** invalid.
+- All tests classified as `valid` **are** valid.
+- All expected outputs in `valid/test-name.json` are exactly correct.
+- The Go standard library package `encoding/json` decodes JSON correctly.
+- When testing encoders, the TOML decoder at
+ [BurntSushi/toml](https://github.com/BurntSushi/toml) is assumed to be
+ correct. (Note that this assumption is not made when testing decoders!)
+
+Of particular note is that **no TOML decoder** is taken as ground truth when
+testing decoders. This means that most changes to the spec will only require an
+update of the tests in `toml-test`. (Bigger changes may require an adjustment of
+how two things are considered equal. Particularly if a new type of data is
+added.) Obviously, this advantage does not apply to testing TOML encoders since
+there must exist a TOML decoder that conforms to the specification in order to
+read the output of a TOML encoder.
+
+Adding tests
+------------
+`toml-test` was designed so that tests can be easily added and removed. As
+mentioned above, tests are split into two groups: invalid and valid tests.
+
+Invalid tests **only check if a decoder rejects invalid TOML data**. Or, in the
+case of testing encoders, invalid tests **only check if an encoder rejects an
+invalid representation of TOML** (e.g., a hetergeneous array). Therefore, all
+invalid tests should try to **test one thing and one thing only**. Invalid tests
+should be named after the fault it is trying to expose. Invalid tests for
+decoders are in the `tests/invalid` directory while invalid tests for encoders
+are in the `tests/invalid-encoder` directory.
+
+Valid tests check that a decoder accepts valid TOML data **and** that the parser
+has the correct representation of the TOML data. Therefore, valid tests need a
+JSON encoding in addition to the TOML data. The tests should be small enough
+that writing the JSON encoding by hand will not give you brain damage. The exact
+reverse is true when testing encoders.
+
+A valid test without either a `.json` or `.toml` file will automatically fail.
+
+If you have tests that you'd like to add, please submit a pull request.
+
+Why JSON?
+---------
+In order for a language agnostic test suite to work, we need some kind of data
+exchange format. TOML cannot be used, as it would imply that a particular parser
+has a blessing of correctness.
+
+My decision to use JSON was not a careful one. It was based on expediency. The
+Go standard library has an excellent `encoding/json` package built in, which
+made it easy to compare JSON data.
+
+The problem with JSON is that the types in TOML are not in one-to-one
+correspondence with JSON. This is why every TOML value represented in JSON is
+tagged with a type annotation, as described above.
+
+YAML may be closer in correspondence with TOML, but I don't believe we should
+rely on that correspondence. Making things explicit with JSON means that writing
+tests is a little more cumbersome, but it also reduces the number of assumptions
+we need to make.
diff --git a/internal/toml-test/json.go b/internal/toml-test/json.go
new file mode 100644
index 0000000..4394f41
--- /dev/null
+++ b/internal/toml-test/json.go
@@ -0,0 +1,258 @@
+//go:build go1.16
+// +build go1.16
+
+package tomltest
+
+import (
+ "strconv"
+ "strings"
+ "time"
+)
+
+// CompareJSON compares the given arguments.
+//
+// The returned value is a copy of Test with Failure set to a (human-readable)
+// description of the first element that is unequal. If both arguments are
+// equal, Test is returned unchanged.
+//
+// reflect.DeepEqual could work here, but it won't tell us how the two
+// structures are different.
+func (r Test) CompareJSON(want, have interface{}) Test {
+ switch w := want.(type) {
+ case map[string]interface{}:
+ return r.cmpJSONMaps(w, have)
+ case []interface{}:
+ return r.cmpJSONArrays(w, have)
+ default:
+ return r.fail(
+ "Key '%s' in expected output should be a map or a list of maps, but it's a %T",
+ r.Key, want)
+ }
+}
+
+func (r Test) cmpJSONMaps(want map[string]interface{}, have interface{}) Test {
+ haveMap, ok := have.(map[string]interface{})
+ if !ok {
+ return r.mismatch("table", want, haveMap)
+ }
+
+ // Check to make sure both or neither are values.
+ if isValue(want) && !isValue(haveMap) {
+ return r.fail(
+ "Key '%s' is supposed to be a value, but the parser reports it as a table",
+ r.Key)
+ }
+ if !isValue(want) && isValue(haveMap) {
+ return r.fail(
+ "Key '%s' is supposed to be a table, but the parser reports it as a value",
+ r.Key)
+ }
+ if isValue(want) && isValue(haveMap) {
+ return r.cmpJSONValues(want, haveMap)
+ }
+
+ // Check that the keys of each map are equivalent.
+ for k := range want {
+ if _, ok := haveMap[k]; !ok {
+ bunk := r.kjoin(k)
+ return bunk.fail("Could not find key '%s' in parser output.",
+ bunk.Key)
+ }
+ }
+ for k := range haveMap {
+ if _, ok := want[k]; !ok {
+ bunk := r.kjoin(k)
+ return bunk.fail("Could not find key '%s' in expected output.",
+ bunk.Key)
+ }
+ }
+
+ // Okay, now make sure that each value is equivalent.
+ for k := range want {
+ if sub := r.kjoin(k).CompareJSON(want[k], haveMap[k]); sub.Failed() {
+ return sub
+ }
+ }
+ return r
+}
+
+func (r Test) cmpJSONArrays(want, have interface{}) Test {
+ wantSlice, ok := want.([]interface{})
+ if !ok {
+ return r.bug("'value' should be a JSON array when 'type=array', but it is a %T", want)
+ }
+
+ haveSlice, ok := have.([]interface{})
+ if !ok {
+ return r.fail(
+ "Malformed output from your encoder: 'value' is not a JSON array: %T", have)
+ }
+
+ if len(wantSlice) != len(haveSlice) {
+ return r.fail("Array lengths differ for key '%s':\n"+
+ " Expected: %d\n"+
+ " Your encoder: %d",
+ r.Key, len(wantSlice), len(haveSlice))
+ }
+ for i := 0; i < len(wantSlice); i++ {
+ if sub := r.CompareJSON(wantSlice[i], haveSlice[i]); sub.Failed() {
+ return sub
+ }
+ }
+ return r
+}
+
+func (r Test) cmpJSONValues(want, have map[string]interface{}) Test {
+ wantType, ok := want["type"].(string)
+ if !ok {
+ return r.bug("'type' should be a string, but it is a %T", want["type"])
+ }
+
+ haveType, ok := have["type"].(string)
+ if !ok {
+ return r.fail("Malformed output from your encoder: 'type' is not a string: %T", have["type"])
+ }
+
+ if wantType != haveType {
+ return r.valMismatch(wantType, haveType, want, have)
+ }
+
+ // If this is an array, then we've got to do some work to check equality.
+ if wantType == "array" {
+ return r.cmpJSONArrays(want, have)
+ }
+
+ // Atomic values are always strings
+ wantVal, ok := want["value"].(string)
+ if !ok {
+ return r.bug("'value' %v should be a string, but it is a %[1]T", want["value"])
+ }
+
+ haveVal, ok := have["value"].(string)
+ if !ok {
+ return r.fail("Malformed output from your encoder: %T is not a string", have["value"])
+ }
+
+ // Excepting floats and datetimes, other values can be compared as strings.
+ switch wantType {
+ case "float":
+ return r.cmpFloats(wantVal, haveVal)
+ case "datetime", "datetime-local", "date-local", "time-local":
+ return r.cmpAsDatetimes(wantType, wantVal, haveVal)
+ default:
+ return r.cmpAsStrings(wantVal, haveVal)
+ }
+}
+
+func (r Test) cmpAsStrings(want, have string) Test {
+ if want != have {
+ return r.fail("Values for key '%s' don't match:\n"+
+ " Expected: %s\n"+
+ " Your encoder: %s",
+ r.Key, want, have)
+ }
+ return r
+}
+
+func (r Test) cmpFloats(want, have string) Test {
+ // Special case for NaN, since NaN != NaN.
+ if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") {
+ if want != have {
+ return r.fail("Values for key '%s' don't match:\n"+
+ " Expected: %v\n"+
+ " Your encoder: %v",
+ r.Key, want, have)
+ }
+ return r
+ }
+
+ wantF, err := strconv.ParseFloat(want, 64)
+ if err != nil {
+ return r.bug("Could not read '%s' as a float value for key '%s'", want, r.Key)
+ }
+
+ haveF, err := strconv.ParseFloat(have, 64)
+ if err != nil {
+ return r.fail("Malformed output from your encoder: key '%s' is not a float: '%s'", r.Key, have)
+ }
+
+ if wantF != haveF {
+ return r.fail("Values for key '%s' don't match:\n"+
+ " Expected: %v\n"+
+ " Your encoder: %v",
+ r.Key, wantF, haveF)
+ }
+ return r
+}
+
+var datetimeRepl = strings.NewReplacer(
+ " ", "T",
+ "t", "T",
+ "z", "Z")
+
+var layouts = map[string]string{
+ "datetime": time.RFC3339Nano,
+ "datetime-local": "2006-01-02T15:04:05.999999999",
+ "date-local": "2006-01-02",
+ "time-local": "15:04:05",
+}
+
+func (r Test) cmpAsDatetimes(kind, want, have string) Test {
+ layout, ok := layouts[kind]
+ if !ok {
+ panic("should never happen")
+ }
+
+ wantT, err := time.Parse(layout, datetimeRepl.Replace(want))
+ if err != nil {
+ return r.bug("Could not read '%s' as a datetime value for key '%s'", want, r.Key)
+ }
+
+ haveT, err := time.Parse(layout, datetimeRepl.Replace(want))
+ if err != nil {
+ return r.fail("Malformed output from your encoder: key '%s' is not a datetime: '%s'", r.Key, have)
+ }
+ if !wantT.Equal(haveT) {
+ return r.fail("Values for key '%s' don't match:\n"+
+ " Expected: %v\n"+
+ " Your encoder: %v",
+ r.Key, wantT, haveT)
+ }
+ return r
+}
+
+func (r Test) kjoin(key string) Test {
+ if len(r.Key) == 0 {
+ r.Key = key
+ } else {
+ r.Key += "." + key
+ }
+ return r
+}
+
+func isValue(m map[string]interface{}) bool {
+ if len(m) != 2 {
+ return false
+ }
+ if _, ok := m["type"]; !ok {
+ return false
+ }
+ if _, ok := m["value"]; !ok {
+ return false
+ }
+ return true
+}
+
+func (r Test) mismatch(wantType string, want, have interface{}) Test {
+ return r.fail("Key '%s' is not an %s but %[4]T:\n"+
+ " Expected: %#[3]v\n"+
+ " Your encoder: %#[4]v",
+ r.Key, wantType, want, have)
+}
+
+func (r Test) valMismatch(wantType, haveType string, want, have interface{}) Test {
+ return r.fail("Key '%s' is not an %s but %s:\n"+
+ " Expected: %#[3]v\n"+
+ " Your encoder: %#[4]v",
+ r.Key, wantType, want, have)
+}
diff --git a/internal/toml-test/runner.go b/internal/toml-test/runner.go
new file mode 100644
index 0000000..2d7e931
--- /dev/null
+++ b/internal/toml-test/runner.go
@@ -0,0 +1,427 @@
+//go:generate ./gen-multi.py
+
+//go:build go1.16
+// +build go1.16
+
+package tomltest
+
+import (
+ "bytes"
+ "embed"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/BurntSushi/toml"
+)
+
+type testType uint8
+
+const (
+ TypeValid testType = iota
+ TypeInvalid
+)
+
+//go:embed tests/*
+var embeddedTests embed.FS
+
+// EmbeddedTests are the tests embedded in toml-test, rooted to the "test/"
+// directory.
+func EmbeddedTests() fs.FS {
+ f, err := fs.Sub(embeddedTests, "tests")
+ if err != nil {
+ panic(err)
+ }
+ return f
+}
+
+// Runner runs a set of tests.
+//
+// The validity of the parameters is not checked extensively; the caller should
+// verify this if need be. See ./cmd/toml-test for an example.
+type Runner struct {
+ Files fs.FS // Test files.
+ Encoder bool // Are we testing an encoder?
+ RunTests []string // Tests to run; run all if blank.
+ SkipTests []string // Tests to skip.
+ Parser Parser // Send data to a parser.
+ Version string // TOML version to run tests for.
+}
+
+// A Parser instance is used to call the TOML parser we test.
+//
+// By default this is done through an external command.
+type Parser interface {
+ // Encode a JSON string to TOML.
+ //
+ // The output is the TOML string; if outputIsError is true then it's assumed
+ // that an encoding error occurred.
+ //
+ // An error return should only be used in case an unrecoverable error
+ // occurred; failing to encode to TOML is not an error, but the encoder
+ // unexpectedly panicking is.
+ Encode(jsonInput string) (output string, outputIsError bool, err error)
+
+ // Decode a TOML string to JSON. The same semantics as Encode apply.
+ Decode(tomlInput string) (output string, outputIsError bool, err error)
+}
+
+// CommandParser calls an external command.
+type CommandParser struct {
+ fsys fs.FS
+ cmd []string
+}
+
+// Tests are tests to run.
+type Tests struct {
+ Tests []Test
+
+ // Set when test are run.
+
+ Skipped, Passed, Failed int
+}
+
+// Result is the result of a single test.
+type Test struct {
+ Path string // Path of test, e.g. "valid/string-test"
+
+ // Set when a test is run.
+
+ Skipped bool // Skipped this test?
+ Failure string // Failure message.
+ Key string // TOML key the failure occured on; may be blank.
+ Encoder bool // Encoder test?
+ Input string // The test case that we sent to the external program.
+ Output string // Output from the external program.
+ Want string // The output we want.
+ OutputFromStderr bool // The Output came from stderr, not stdout.
+}
+
+// List all tests in Files for the current TOML version.
+func (r Runner) List() ([]string, error) {
+ if r.Version == "" {
+ r.Version = "1.0.0"
+ }
+ if _, ok := versions[r.Version]; !ok {
+ v := make([]string, 0, len(versions))
+ for k := range versions {
+ v = append(v, k)
+ }
+ sort.Strings(v)
+ return nil, fmt.Errorf("tomltest.Runner.Run: unknown version: %q (supported: \"%s\")",
+ r.Version, strings.Join(v, `", "`))
+ }
+
+ var (
+ v = versions[r.Version]
+ exclude = make([]string, 0, 8)
+ )
+ for {
+ exclude = append(exclude, v.exclude...)
+ if v.inherit == "" {
+ break
+ }
+ v = versions[v.inherit]
+ }
+
+ ls := make([]string, 0, 256)
+ if err := r.findTOML("valid", &ls, exclude); err != nil {
+ return nil, fmt.Errorf("reading 'valid/' dir: %w", err)
+ }
+
+ d := "invalid" + map[bool]string{true: "-encoder", false: ""}[r.Encoder]
+ if err := r.findTOML(d, &ls, exclude); err != nil {
+ return nil, fmt.Errorf("reading %q dir: %w", d, err)
+ }
+
+ return ls, nil
+}
+
+// Run all tests listed in t.RunTests.
+//
+// TODO: give option to:
+// - Run all tests with \n replaced with \r\n
+// - Run all tests with EOL removed
+// - Run all tests with '# comment' appended to every line.
+func (r Runner) Run() (Tests, error) {
+ skipped, err := r.findTests()
+ if err != nil {
+ return Tests{}, fmt.Errorf("tomltest.Runner.Run: %w", err)
+ }
+
+ tests := Tests{Tests: make([]Test, 0, len(r.RunTests)), Skipped: skipped}
+ for _, p := range r.RunTests {
+ if r.hasSkip(p) {
+ tests.Skipped++
+ tests.Tests = append(tests.Tests, Test{Path: p, Skipped: true, Encoder: r.Encoder})
+ continue
+ }
+
+ t := Test{Path: p, Encoder: r.Encoder}.Run(r.Parser, r.Files)
+ tests.Tests = append(tests.Tests, t)
+
+ if t.Failed() {
+ tests.Failed++
+ } else {
+ tests.Passed++
+ }
+ }
+
+ return tests, nil
+}
+
+// find all TOML files in 'path' relative to the test directory.
+func (r Runner) findTOML(path string, appendTo *[]string, exclude []string) error {
+ // It's okay if the directory doesn't exist.
+ if _, err := fs.Stat(r.Files, path); errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+
+ return fs.WalkDir(r.Files, path, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() || !strings.HasSuffix(path, ".toml") {
+ return nil
+ }
+
+ path = strings.TrimSuffix(path, ".toml")
+ for _, e := range exclude {
+ if ok, _ := filepath.Match(e, path); ok {
+ return nil
+ }
+ }
+
+ *appendTo = append(*appendTo, path)
+ return nil
+ })
+}
+
+// Expand RunTest glob patterns, or return all tests if RunTests if empty.
+func (r *Runner) findTests() (int, error) {
+ ls, err := r.List()
+ if err != nil {
+ return 0, err
+ }
+
+ var skip int
+
+ if len(r.RunTests) == 0 {
+ r.RunTests = ls
+ } else {
+ run := make([]string, 0, len(r.RunTests))
+ for _, l := range ls {
+ for _, r := range r.RunTests {
+ if m, _ := filepath.Match(r, l); m {
+ run = append(run, l)
+ break
+ }
+ }
+ }
+ r.RunTests, skip = run, len(ls)-len(run)
+ }
+
+ // Expand invalid tests ending in ".multi.toml"
+ expanded := make([]string, 0, len(r.RunTests))
+ for _, path := range r.RunTests {
+ if !strings.HasSuffix(path, ".multi") {
+ expanded = append(expanded, path)
+ continue
+ }
+
+ d, err := fs.ReadFile(r.Files, path+".toml")
+ if err != nil {
+ return 0, err
+ }
+
+ fmt.Println(string(d))
+ }
+ r.RunTests = expanded
+
+ return skip, nil
+}
+
+func (r Runner) hasSkip(path string) bool {
+ for _, s := range r.SkipTests {
+ if m, _ := filepath.Match(s, path); m {
+ return true
+ }
+ }
+ return false
+}
+
+func (c CommandParser) Encode(input string) (output string, outputIsError bool, err error) {
+ stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
+ cmd := exec.Command(c.cmd[0])
+ cmd.Args = c.cmd
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = strings.NewReader(input), stdout, stderr
+
+ err = cmd.Run()
+ if err != nil {
+ eErr := &exec.ExitError{}
+ if errors.As(err, &eErr) {
+ fmt.Fprintf(stderr, "\nExit %d\n", eErr.ProcessState.ExitCode())
+ err = nil
+ }
+ }
+
+ if stderr.Len() > 0 {
+ return strings.TrimSpace(stderr.String()) + "\n", true, err
+ }
+ return strings.TrimSpace(stdout.String()) + "\n", false, err
+}
+func NewCommandParser(fsys fs.FS, cmd []string) CommandParser { return CommandParser{fsys, cmd} }
+func (c CommandParser) Decode(input string) (string, bool, error) { return c.Encode(input) }
+
+// Run this test.
+func (t Test) Run(p Parser, fsys fs.FS) Test {
+ if t.Type() == TypeInvalid {
+ return t.runInvalid(p, fsys)
+ }
+ return t.runValid(p, fsys)
+}
+
+func (t Test) runInvalid(p Parser, fsys fs.FS) Test {
+ var err error
+ _, t.Input, err = t.ReadInput(fsys)
+ if err != nil {
+ return t.bug(err.Error())
+ }
+
+ if t.Encoder {
+ t.Output, t.OutputFromStderr, err = p.Encode(t.Input)
+ } else {
+ t.Output, t.OutputFromStderr, err = p.Decode(t.Input)
+ }
+ if err != nil {
+ return t.fail(err.Error())
+ }
+ if !t.OutputFromStderr {
+ return t.fail("Expected an error, but no error was reported.")
+ }
+ return t
+}
+
+func (t Test) runValid(p Parser, fsys fs.FS) Test {
+ var err error
+ _, t.Input, err = t.ReadInput(fsys)
+ if err != nil {
+ return t.bug(err.Error())
+ }
+
+ if t.Encoder {
+ t.Output, t.OutputFromStderr, err = p.Encode(t.Input)
+ } else {
+ t.Output, t.OutputFromStderr, err = p.Decode(t.Input)
+ }
+ if err != nil {
+ return t.fail(err.Error())
+ }
+ if t.OutputFromStderr {
+ return t.fail(t.Output)
+ }
+ if t.Output == "" {
+ // Special case: we expect an empty output here.
+ if t.Path != "valid/empty-file" {
+ return t.fail("stdout is empty")
+ }
+ }
+
+ // Compare for encoder test
+ if t.Encoder {
+ want, err := t.ReadWantTOML(fsys)
+ if err != nil {
+ return t.bug(err.Error())
+ }
+ var have interface{}
+ if _, err := toml.Decode(t.Output, &have); err != nil {
+ //return t.fail("decode TOML from encoder %q:\n %s", cmd, err)
+ return t.fail("decode TOML from encoder:\n %s", err)
+ }
+ return t.CompareTOML(want, have)
+ }
+
+ // Compare for decoder test
+ want, err := t.ReadWantJSON(fsys)
+ if err != nil {
+ return t.fail(err.Error())
+ }
+
+ var have interface{}
+ if err := json.Unmarshal([]byte(t.Output), &have); err != nil {
+ return t.fail("decode JSON output from parser:\n %s", err)
+ }
+
+ return t.CompareJSON(want, have)
+}
+
+// ReadInput reads the file sent to the encoder.
+func (t Test) ReadInput(fsys fs.FS) (path, data string, err error) {
+ path = t.Path + map[bool]string{true: ".json", false: ".toml"}[t.Encoder]
+ d, err := fs.ReadFile(fsys, path)
+ if err != nil {
+ return path, "", err
+ }
+ return path, string(d), nil
+}
+
+func (t Test) ReadWant(fsys fs.FS) (path, data string, err error) {
+ if t.Type() == TypeInvalid {
+ panic("testoml.Test.ReadWant: invalid tests do not have a 'correct' version")
+ }
+
+ path = t.Path + map[bool]string{true: ".toml", false: ".json"}[t.Encoder]
+ d, err := fs.ReadFile(fsys, path)
+ if err != nil {
+ return path, "", err
+ }
+ return path, string(d), nil
+}
+
+func (t *Test) ReadWantJSON(fsys fs.FS) (v interface{}, err error) {
+ var path string
+ path, t.Want, err = t.ReadWant(fsys)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := json.Unmarshal([]byte(t.Want), &v); err != nil {
+ return nil, fmt.Errorf("decode JSON file %q:\n %s", path, err)
+ }
+ return v, nil
+}
+func (t *Test) ReadWantTOML(fsys fs.FS) (v interface{}, err error) {
+ var path string
+ path, t.Want, err = t.ReadWant(fsys)
+ if err != nil {
+ return nil, err
+ }
+ _, err = toml.Decode(t.Want, &v)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode TOML file %q:\n %s", path, err)
+ }
+ return v, nil
+}
+
+// Test type: "valid", "invalid"
+func (t Test) Type() testType {
+ if strings.HasPrefix(t.Path, "invalid") {
+ return TypeInvalid
+ }
+ return TypeValid
+}
+
+func (t Test) fail(format string, v ...interface{}) Test {
+ t.Failure = fmt.Sprintf(format, v...)
+ return t
+}
+func (t Test) bug(format string, v ...interface{}) Test {
+ return t.fail("BUG IN TEST CASE: "+format, v...)
+}
+
+func (t Test) Failed() bool { return t.Failure != "" }
diff --git a/internal/toml-test/tests/.gitattributes b/internal/toml-test/tests/.gitattributes
new file mode 100644
index 0000000..6381655
--- /dev/null
+++ b/internal/toml-test/tests/.gitattributes
@@ -0,0 +1 @@
+*.toml -text
diff --git a/internal/toml-test/tests/invalid/array/double-comma-1.toml b/internal/toml-test/tests/invalid/array/double-comma-1.toml
new file mode 100644
index 0000000..339aad2
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/double-comma-1.toml
@@ -0,0 +1 @@
+array = [1,,2]
diff --git a/internal/toml-test/tests/invalid/array/double-comma-2.toml b/internal/toml-test/tests/invalid/array/double-comma-2.toml
new file mode 100644
index 0000000..ce3565c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/double-comma-2.toml
@@ -0,0 +1,2 @@
+array = [1,2,,]
+
diff --git a/internal/toml-test/tests/invalid/array/extending-table.toml b/internal/toml-test/tests/invalid/array/extending-table.toml
new file mode 100644
index 0000000..a6b7ce4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/extending-table.toml
@@ -0,0 +1,6 @@
+a = [{ b = 1 }]
+
+# Cannot extend tables within static arrays
+# https://github.com/toml-lang/toml/issues/908
+[a.c]
+foo = 1
diff --git a/internal/toml-test/tests/invalid/array/missing-separator.toml b/internal/toml-test/tests/invalid/array/missing-separator.toml
new file mode 100644
index 0000000..ab3791a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/missing-separator.toml
@@ -0,0 +1 @@
+wrong = [ 1 2 3 ]
diff --git a/internal/toml-test/tests/invalid/array/no-close-2.toml b/internal/toml-test/tests/invalid/array/no-close-2.toml
new file mode 100644
index 0000000..2f46123
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/no-close-2.toml
@@ -0,0 +1 @@
+x = [42 #
diff --git a/internal/toml-test/tests/invalid/array/no-close-table-2.toml b/internal/toml-test/tests/invalid/array/no-close-table-2.toml
new file mode 100644
index 0000000..827c128
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/no-close-table-2.toml
@@ -0,0 +1 @@
+x = [{ key = 42 #
diff --git a/internal/toml-test/tests/invalid/array/no-close-table.toml b/internal/toml-test/tests/invalid/array/no-close-table.toml
new file mode 100644
index 0000000..7b077be
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/no-close-table.toml
@@ -0,0 +1 @@
+x = [{ key = 42
diff --git a/internal/toml-test/tests/invalid/array/no-close.toml b/internal/toml-test/tests/invalid/array/no-close.toml
new file mode 100644
index 0000000..032ebcc
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/no-close.toml
@@ -0,0 +1 @@
+long_array = [ 1, 2, 3
diff --git a/internal/toml-test/tests/invalid/array/tables-1.toml b/internal/toml-test/tests/invalid/array/tables-1.toml
new file mode 100644
index 0000000..01d4d13
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/tables-1.toml
@@ -0,0 +1,4 @@
+# INVALID TOML DOC
+fruit = []
+
+[[fruit]] # Not allowed
diff --git a/internal/toml-test/tests/invalid/array/tables-2.toml b/internal/toml-test/tests/invalid/array/tables-2.toml
new file mode 100644
index 0000000..a77b0e4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/tables-2.toml
@@ -0,0 +1,10 @@
+# INVALID TOML DOC
+[[fruit]]
+ name = "apple"
+
+ [[fruit.variety]]
+ name = "red delicious"
+
+ # This table conflicts with the previous table
+ [fruit.variety]
+ name = "granny smith"
diff --git a/internal/toml-test/tests/invalid/array/text-after-array-entries.toml b/internal/toml-test/tests/invalid/array/text-after-array-entries.toml
new file mode 100644
index 0000000..1a72890
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/text-after-array-entries.toml
@@ -0,0 +1,4 @@
+array = [
+ "Is there life after an array separator?", No
+ "Entry"
+]
diff --git a/internal/toml-test/tests/invalid/array/text-before-array-separator.toml b/internal/toml-test/tests/invalid/array/text-before-array-separator.toml
new file mode 100644
index 0000000..9b06a39
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/text-before-array-separator.toml
@@ -0,0 +1,4 @@
+array = [
+ "Is there life before an array separator?" No,
+ "Entry"
+]
diff --git a/internal/toml-test/tests/invalid/array/text-in-array.toml b/internal/toml-test/tests/invalid/array/text-in-array.toml
new file mode 100644
index 0000000..a6a6c42
--- /dev/null
+++ b/internal/toml-test/tests/invalid/array/text-in-array.toml
@@ -0,0 +1,5 @@
+array = [
+ "Entry 1",
+ I don't belong,
+ "Entry 2",
+]
diff --git a/internal/toml-test/tests/invalid/bool/almost-false-with-extra.toml b/internal/toml-test/tests/invalid/bool/almost-false-with-extra.toml
new file mode 100644
index 0000000..047911e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/almost-false-with-extra.toml
@@ -0,0 +1 @@
+a = falsify
diff --git a/internal/toml-test/tests/invalid/bool/almost-false.toml b/internal/toml-test/tests/invalid/bool/almost-false.toml
new file mode 100644
index 0000000..90d612d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/almost-false.toml
@@ -0,0 +1 @@
+a = fals
diff --git a/internal/toml-test/tests/invalid/bool/almost-true-with-extra.toml b/internal/toml-test/tests/invalid/bool/almost-true-with-extra.toml
new file mode 100644
index 0000000..b8946f3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/almost-true-with-extra.toml
@@ -0,0 +1 @@
+a = truthy
diff --git a/internal/toml-test/tests/invalid/bool/almost-true.toml b/internal/toml-test/tests/invalid/bool/almost-true.toml
new file mode 100644
index 0000000..e9a6145
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/almost-true.toml
@@ -0,0 +1 @@
+a = tru
diff --git a/internal/toml-test/tests/invalid/bool/just-f.toml b/internal/toml-test/tests/invalid/bool/just-f.toml
new file mode 100644
index 0000000..b7dfb90
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/just-f.toml
@@ -0,0 +1 @@
+a = f
diff --git a/internal/toml-test/tests/invalid/bool/just-t.toml b/internal/toml-test/tests/invalid/bool/just-t.toml
new file mode 100644
index 0000000..26e457a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/just-t.toml
@@ -0,0 +1 @@
+a = t
diff --git a/internal/toml-test/tests/invalid/bool/mixed-case.toml b/internal/toml-test/tests/invalid/bool/mixed-case.toml
new file mode 100644
index 0000000..41d995a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/mixed-case.toml
@@ -0,0 +1 @@
+valid = False
diff --git a/internal/toml-test/tests/invalid/bool/starting-same-false.toml b/internal/toml-test/tests/invalid/bool/starting-same-false.toml
new file mode 100644
index 0000000..19dfb8d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/starting-same-false.toml
@@ -0,0 +1 @@
+a = falsey
diff --git a/internal/toml-test/tests/invalid/bool/starting-same-true.toml b/internal/toml-test/tests/invalid/bool/starting-same-true.toml
new file mode 100644
index 0000000..bf2f784
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/starting-same-true.toml
@@ -0,0 +1 @@
+a = truer
diff --git a/internal/toml-test/tests/invalid/bool/wrong-case-false.toml b/internal/toml-test/tests/invalid/bool/wrong-case-false.toml
new file mode 100644
index 0000000..3982220
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/wrong-case-false.toml
@@ -0,0 +1 @@
+b = FALSE
diff --git a/internal/toml-test/tests/invalid/bool/wrong-case-true.toml b/internal/toml-test/tests/invalid/bool/wrong-case-true.toml
new file mode 100644
index 0000000..85e34c5
--- /dev/null
+++ b/internal/toml-test/tests/invalid/bool/wrong-case-true.toml
@@ -0,0 +1 @@
+a = TRUE
diff --git a/internal/toml-test/tests/invalid/control/bare-cr.toml b/internal/toml-test/tests/invalid/control/bare-cr.toml
new file mode 100644
index 0000000..37f4ce4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/bare-cr.toml
@@ -0,0 +1,2 @@
+# The following line contains a single carriage return control character
+ \ No newline at end of file
diff --git a/internal/toml-test/tests/invalid/control/bare-formfeed.toml b/internal/toml-test/tests/invalid/control/bare-formfeed.toml
new file mode 100644
index 0000000..508a650
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/bare-formfeed.toml
@@ -0,0 +1 @@
+bare-formfeed =
diff --git a/internal/toml-test/tests/invalid/control/bare-null.toml b/internal/toml-test/tests/invalid/control/bare-null.toml
new file mode 100644
index 0000000..cf15002
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/bare-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/bare-vertical-tab.toml b/internal/toml-test/tests/invalid/control/bare-vertical-tab.toml
new file mode 100644
index 0000000..abacefd
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/bare-vertical-tab.toml
@@ -0,0 +1 @@
+bare-vertical-tab =
diff --git a/internal/toml-test/tests/invalid/control/comment-cr.toml b/internal/toml-test/tests/invalid/control/comment-cr.toml
new file mode 100644
index 0000000..3ce1cb4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/comment-cr.toml
@@ -0,0 +1 @@
+comment-cr = "Carriage return in comment" # a=1
diff --git a/internal/toml-test/tests/invalid/control/comment-del.toml b/internal/toml-test/tests/invalid/control/comment-del.toml
new file mode 100644
index 0000000..fb90400
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/comment-del.toml
@@ -0,0 +1 @@
+comment-del = "0x7f" # 
diff --git a/internal/toml-test/tests/invalid/control/comment-lf.toml b/internal/toml-test/tests/invalid/control/comment-lf.toml
new file mode 100644
index 0000000..f6e963e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/comment-lf.toml
@@ -0,0 +1 @@
+comment-lf = "ctrl-P" # 
diff --git a/internal/toml-test/tests/invalid/control/comment-null.toml b/internal/toml-test/tests/invalid/control/comment-null.toml
new file mode 100644
index 0000000..2c90372
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/comment-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/comment-us.toml b/internal/toml-test/tests/invalid/control/comment-us.toml
new file mode 100644
index 0000000..c685b79
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/comment-us.toml
@@ -0,0 +1 @@
+comment-us = "ctrl-_" # 
diff --git a/internal/toml-test/tests/invalid/control/control.multi b/internal/toml-test/tests/invalid/control/control.multi
new file mode 100644
index 0000000..d23b6a6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/control.multi
@@ -0,0 +1,33 @@
+# "\x.." sequences are replaced with literal control characters.
+
+comment-null = "null" # \x00
+comment-lf = "ctrl-P" # \x10
+comment-us = "ctrl-_" # \x1f
+comment-del = "0x7f" # \x7f
+comment-cr = "Carriage return in comment" # \x0da=1
+
+string-null = "null\x00"
+string-lf = "null\x10"
+string-us = "null\x1f"
+string-del = "null\x7f"
+
+rawstring-null = 'null\x00'
+rawstring-lf = 'null\x10'
+rawstring-us = 'null\x1f'
+rawstring-del = 'null\x7f'
+
+multi-null = """null\x00"""
+multi-lf = """null\x10"""
+multi-us = """null\x1f"""
+multi-del = """null\x7f"""
+
+rawmulti-null = '''null\x00'''
+rawmulti-lf = '''null\x10'''
+rawmulti-us = '''null\x1f'''
+rawmulti-del = '''null\x7f'''
+
+string-bs = "backspace\x08"
+
+bare-null = "some value" \x00
+bare-formfeed = \x0c
+bare-vertical-tab = \x0b
diff --git a/internal/toml-test/tests/invalid/control/multi-del.toml b/internal/toml-test/tests/invalid/control/multi-del.toml
new file mode 100644
index 0000000..6629ae3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/multi-del.toml
@@ -0,0 +1 @@
+multi-del = """null"""
diff --git a/internal/toml-test/tests/invalid/control/multi-lf.toml b/internal/toml-test/tests/invalid/control/multi-lf.toml
new file mode 100644
index 0000000..f207d33
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/multi-lf.toml
@@ -0,0 +1 @@
+multi-lf = """null"""
diff --git a/internal/toml-test/tests/invalid/control/multi-null.toml b/internal/toml-test/tests/invalid/control/multi-null.toml
new file mode 100644
index 0000000..ac6cb46
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/multi-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/multi-us.toml b/internal/toml-test/tests/invalid/control/multi-us.toml
new file mode 100644
index 0000000..b923ca9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/multi-us.toml
@@ -0,0 +1 @@
+multi-us = """null"""
diff --git a/internal/toml-test/tests/invalid/control/rawmulti-del.toml b/internal/toml-test/tests/invalid/control/rawmulti-del.toml
new file mode 100644
index 0000000..17cde07
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawmulti-del.toml
@@ -0,0 +1 @@
+rawmulti-del = '''null'''
diff --git a/internal/toml-test/tests/invalid/control/rawmulti-lf.toml b/internal/toml-test/tests/invalid/control/rawmulti-lf.toml
new file mode 100644
index 0000000..360d489
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawmulti-lf.toml
@@ -0,0 +1 @@
+rawmulti-lf = '''null'''
diff --git a/internal/toml-test/tests/invalid/control/rawmulti-null.toml b/internal/toml-test/tests/invalid/control/rawmulti-null.toml
new file mode 100644
index 0000000..1b5bd09
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawmulti-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/rawmulti-us.toml b/internal/toml-test/tests/invalid/control/rawmulti-us.toml
new file mode 100644
index 0000000..0251420
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawmulti-us.toml
@@ -0,0 +1 @@
+rawmulti-us = '''null'''
diff --git a/internal/toml-test/tests/invalid/control/rawstring-del.toml b/internal/toml-test/tests/invalid/control/rawstring-del.toml
new file mode 100644
index 0000000..8d83ba8
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawstring-del.toml
@@ -0,0 +1 @@
+rawstring-del = 'null'
diff --git a/internal/toml-test/tests/invalid/control/rawstring-lf.toml b/internal/toml-test/tests/invalid/control/rawstring-lf.toml
new file mode 100644
index 0000000..237470a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawstring-lf.toml
@@ -0,0 +1 @@
+rawstring-lf = 'null'
diff --git a/internal/toml-test/tests/invalid/control/rawstring-null.toml b/internal/toml-test/tests/invalid/control/rawstring-null.toml
new file mode 100644
index 0000000..2d8d0b4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawstring-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/rawstring-us.toml b/internal/toml-test/tests/invalid/control/rawstring-us.toml
new file mode 100644
index 0000000..76cb08c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/rawstring-us.toml
@@ -0,0 +1 @@
+rawstring-us = 'null'
diff --git a/internal/toml-test/tests/invalid/control/string-bs.toml b/internal/toml-test/tests/invalid/control/string-bs.toml
new file mode 100644
index 0000000..0061c48
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/string-bs.toml
@@ -0,0 +1 @@
+string-bs = "backspace"
diff --git a/internal/toml-test/tests/invalid/control/string-del.toml b/internal/toml-test/tests/invalid/control/string-del.toml
new file mode 100644
index 0000000..bc0ec7f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/string-del.toml
@@ -0,0 +1 @@
+string-del = "null"
diff --git a/internal/toml-test/tests/invalid/control/string-lf.toml b/internal/toml-test/tests/invalid/control/string-lf.toml
new file mode 100644
index 0000000..0cf9218
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/string-lf.toml
@@ -0,0 +1 @@
+string-lf = "null"
diff --git a/internal/toml-test/tests/invalid/control/string-null.toml b/internal/toml-test/tests/invalid/control/string-null.toml
new file mode 100644
index 0000000..821c72c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/string-null.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/control/string-us.toml b/internal/toml-test/tests/invalid/control/string-us.toml
new file mode 100644
index 0000000..af31f57
--- /dev/null
+++ b/internal/toml-test/tests/invalid/control/string-us.toml
@@ -0,0 +1 @@
+string-us = "null"
diff --git a/internal/toml-test/tests/invalid/datetime/hour-over.toml b/internal/toml-test/tests/invalid/datetime/hour-over.toml
new file mode 100644
index 0000000..d4b89b6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/hour-over.toml
@@ -0,0 +1,2 @@
+# time-hour = 2DIGIT ; 00-23
+d = 2006-01-01T24:00:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/mday-over.toml b/internal/toml-test/tests/invalid/datetime/mday-over.toml
new file mode 100644
index 0000000..600a409
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/mday-over.toml
@@ -0,0 +1,3 @@
+# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
+# ; month/year
+d = 2006-01-32T00:00:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/mday-under.toml b/internal/toml-test/tests/invalid/datetime/mday-under.toml
new file mode 100644
index 0000000..689fc98
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/mday-under.toml
@@ -0,0 +1,3 @@
+# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
+# ; month/year
+d = 2006-01-00T00:00:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/minute-over.toml b/internal/toml-test/tests/invalid/datetime/minute-over.toml
new file mode 100644
index 0000000..1532d79
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/minute-over.toml
@@ -0,0 +1,2 @@
+# time-minute = 2DIGIT ; 00-59
+d = 2006-01-01T00:60:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/month-over.toml b/internal/toml-test/tests/invalid/datetime/month-over.toml
new file mode 100644
index 0000000..c7d40c9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/month-over.toml
@@ -0,0 +1,2 @@
+# date-month = 2DIGIT ; 01-12
+d = 2006-13-01T00:00:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/month-under.toml b/internal/toml-test/tests/invalid/datetime/month-under.toml
new file mode 100644
index 0000000..503f60a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/month-under.toml
@@ -0,0 +1,2 @@
+# date-month = 2DIGIT ; 01-12
+d = 2007-00-01T00:00:00-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/no-leads-with-milli.toml b/internal/toml-test/tests/invalid/datetime/no-leads-with-milli.toml
new file mode 100644
index 0000000..6e3d2e3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/no-leads-with-milli.toml
@@ -0,0 +1,2 @@
+# Day "5" instead of "05"; the leading zero is required.
+with-milli = 1987-07-5T17:45:00.12Z
diff --git a/internal/toml-test/tests/invalid/datetime/no-leads.toml b/internal/toml-test/tests/invalid/datetime/no-leads.toml
new file mode 100644
index 0000000..6700d1c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/no-leads.toml
@@ -0,0 +1,2 @@
+# Month "7" instead of "07"; the leading zero is required.
+no-leads = 1987-7-05T17:45:00Z
diff --git a/internal/toml-test/tests/invalid/datetime/no-secs.toml b/internal/toml-test/tests/invalid/datetime/no-secs.toml
new file mode 100644
index 0000000..bbd196a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/no-secs.toml
@@ -0,0 +1,2 @@
+# No seconds in time.
+no-secs = 1987-07-05T17:45Z
diff --git a/internal/toml-test/tests/invalid/datetime/no-t.toml b/internal/toml-test/tests/invalid/datetime/no-t.toml
new file mode 100644
index 0000000..8d87260
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/no-t.toml
@@ -0,0 +1,2 @@
+# No "t" or "T" between the date and time.
+no-t = 1987-07-0517:45:00Z
diff --git a/internal/toml-test/tests/invalid/datetime/second-over.toml b/internal/toml-test/tests/invalid/datetime/second-over.toml
new file mode 100644
index 0000000..4921704
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/second-over.toml
@@ -0,0 +1,3 @@
+# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
+# ; rules
+d = 2006-01-01T00:00:61-00:00
diff --git a/internal/toml-test/tests/invalid/datetime/time-no-leads-2.toml b/internal/toml-test/tests/invalid/datetime/time-no-leads-2.toml
new file mode 100644
index 0000000..d7b8459
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/time-no-leads-2.toml
@@ -0,0 +1,2 @@
+# Leading 0 is always required.
+d = 01:32:0
diff --git a/internal/toml-test/tests/invalid/datetime/time-no-leads.toml b/internal/toml-test/tests/invalid/datetime/time-no-leads.toml
new file mode 100644
index 0000000..8ad722a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/time-no-leads.toml
@@ -0,0 +1,2 @@
+# Leading 0 is always required.
+d = 1:32:00
diff --git a/internal/toml-test/tests/invalid/datetime/trailing-t.toml b/internal/toml-test/tests/invalid/datetime/trailing-t.toml
new file mode 100644
index 0000000..38ee1db
--- /dev/null
+++ b/internal/toml-test/tests/invalid/datetime/trailing-t.toml
@@ -0,0 +1,2 @@
+# Date cannot end with trailing T
+d = 2006-01-30T
diff --git a/internal/toml-test/tests/invalid/encoding/bad-codepoint.toml b/internal/toml-test/tests/invalid/encoding/bad-codepoint.toml
new file mode 100644
index 0000000..b5b819a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-codepoint.toml
@@ -0,0 +1 @@
+# Invalid codepoint U+D800 :
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-at-end.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-at-end.toml
new file mode 100644
index 0000000..df5f0b0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-at-end.toml
@@ -0,0 +1,5 @@
+# There is a 0xda at after the quotes, and no EOL at the end of the file.
+#
+# This is a bit of an edge case: This indicates there should be two bytes
+# (0b1101_1010) but there is no byte to follow because it's the end of the file.
+x = """""" \ No newline at end of file
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-in-comment.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-comment.toml
new file mode 100644
index 0000000..816fa46
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-comment.toml
@@ -0,0 +1 @@
+#
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline-literal.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline-literal.toml
new file mode 100644
index 0000000..296cf9d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline-literal.toml
@@ -0,0 +1,2 @@
+# The following line contains an invalid UTF-8 sequence.
+bad = ''''''
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline.toml
new file mode 100644
index 0000000..9371cb1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-multiline.toml
@@ -0,0 +1,2 @@
+# The following line contains an invalid UTF-8 sequence.
+bad = """"""
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string-literal.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string-literal.toml
new file mode 100644
index 0000000..3f8948b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string-literal.toml
@@ -0,0 +1,2 @@
+# The following line contains an invalid UTF-8 sequence.
+bad = ''
diff --git a/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string.toml b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string.toml
new file mode 100644
index 0000000..6d376fb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bad-utf8-in-string.toml
@@ -0,0 +1,2 @@
+# The following line contains an invalid UTF-8 sequence.
+bad = ""
diff --git a/internal/toml-test/tests/invalid/encoding/bom-not-at-start-1.toml b/internal/toml-test/tests/invalid/encoding/bom-not-at-start-1.toml
new file mode 100644
index 0000000..effe1f2
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bom-not-at-start-1.toml
@@ -0,0 +1 @@
+bom-not-at-start
diff --git a/internal/toml-test/tests/invalid/encoding/bom-not-at-start-2.toml b/internal/toml-test/tests/invalid/encoding/bom-not-at-start-2.toml
new file mode 100644
index 0000000..71e52b4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/bom-not-at-start-2.toml
@@ -0,0 +1 @@
+bom-not-at-start=
diff --git a/internal/toml-test/tests/invalid/encoding/utf16-bom.toml b/internal/toml-test/tests/invalid/encoding/utf16-bom.toml
new file mode 100644
index 0000000..95b5f96
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/utf16-bom.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/encoding/utf16.toml b/internal/toml-test/tests/invalid/encoding/utf16.toml
new file mode 100644
index 0000000..de3cbb2
--- /dev/null
+++ b/internal/toml-test/tests/invalid/encoding/utf16.toml
Binary files differ
diff --git a/internal/toml-test/tests/invalid/float/double-point-1.toml b/internal/toml-test/tests/invalid/float/double-point-1.toml
new file mode 100644
index 0000000..2105fa9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/double-point-1.toml
@@ -0,0 +1 @@
+double-point-1 = 0..1
diff --git a/internal/toml-test/tests/invalid/float/double-point-2.toml b/internal/toml-test/tests/invalid/float/double-point-2.toml
new file mode 100644
index 0000000..1ef3d0b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/double-point-2.toml
@@ -0,0 +1 @@
+double-point-2 = 0.1.2
diff --git a/internal/toml-test/tests/invalid/float/exp-double-e-1.toml b/internal/toml-test/tests/invalid/float/exp-double-e-1.toml
new file mode 100644
index 0000000..ee568ce
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-double-e-1.toml
@@ -0,0 +1 @@
+exp-double-e-1 = 1ee2
diff --git a/internal/toml-test/tests/invalid/float/exp-double-e-2.toml b/internal/toml-test/tests/invalid/float/exp-double-e-2.toml
new file mode 100644
index 0000000..8fb0158
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-double-e-2.toml
@@ -0,0 +1 @@
+exp-double-e-2 = 1e2e3
diff --git a/internal/toml-test/tests/invalid/float/exp-double-us.toml b/internal/toml-test/tests/invalid/float/exp-double-us.toml
new file mode 100644
index 0000000..63f94e6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-double-us.toml
@@ -0,0 +1 @@
+exp-double-us = 1e__23
diff --git a/internal/toml-test/tests/invalid/float/exp-leading-us.toml b/internal/toml-test/tests/invalid/float/exp-leading-us.toml
new file mode 100644
index 0000000..1426f36
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-leading-us.toml
@@ -0,0 +1 @@
+exp-leading-us = 1e_23
diff --git a/internal/toml-test/tests/invalid/float/exp-point-1.toml b/internal/toml-test/tests/invalid/float/exp-point-1.toml
new file mode 100644
index 0000000..569590b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-point-1.toml
@@ -0,0 +1 @@
+exp-point-1 = 1e2.3
diff --git a/internal/toml-test/tests/invalid/float/exp-point-2.toml b/internal/toml-test/tests/invalid/float/exp-point-2.toml
new file mode 100644
index 0000000..4a5e1d4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-point-2.toml
@@ -0,0 +1 @@
+exp-point-2 = 1.e2
diff --git a/internal/toml-test/tests/invalid/float/exp-trailing-us.toml b/internal/toml-test/tests/invalid/float/exp-trailing-us.toml
new file mode 100644
index 0000000..07c0641
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/exp-trailing-us.toml
@@ -0,0 +1 @@
+exp-trailing-us = 1e_23_
diff --git a/internal/toml-test/tests/invalid/float/float.multi b/internal/toml-test/tests/invalid/float/float.multi
new file mode 100644
index 0000000..0708195
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/float.multi
@@ -0,0 +1,40 @@
+leading-zero = 03.14
+leading-zero-neg = -03.14
+leading-zero-plus = +03.14
+
+leading-point = .12345
+leading-point-neg = -.12345
+leading-point-plus = +.12345
+
+trailing-point = 1.
+trailing-point-min = -1.
+trailing-point-plus = +1.
+
+trailing-us = 1.2_
+leading-us = _1.2
+us-before-point = 1_.2
+us-after-point = 1._2
+
+double-point-1 = 0..1
+double-point-2 = 0.1.2
+
+exp-point-1 = 1e2.3
+exp-point-2 = 1.e2
+
+exp-double-e-1 = 1ee2
+exp-double-e-2 = 1e2e3
+
+exp-leading-us = 1e_23
+exp-trailing-us = 1e_23_
+exp-double-us = 1e__23
+
+inf-incomplete-1 = in
+inf-incomplete-2 = +in
+inf-incomplete-3 = -in
+
+nan-incomplete-1 = na
+nan-incomplete-2 = +na
+nan-incomplete-3 = -na
+
+nan_underscore = na_n
+inf_underscore = in_f
diff --git a/internal/toml-test/tests/invalid/float/inf-capital.toml b/internal/toml-test/tests/invalid/float/inf-capital.toml
new file mode 100644
index 0000000..2c0cb4e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/inf-capital.toml
@@ -0,0 +1 @@
+v = Inf
diff --git a/internal/toml-test/tests/invalid/float/inf-incomplete-1.toml b/internal/toml-test/tests/invalid/float/inf-incomplete-1.toml
new file mode 100644
index 0000000..2c9ef59
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/inf-incomplete-1.toml
@@ -0,0 +1 @@
+inf-incomplete-1 = in
diff --git a/internal/toml-test/tests/invalid/float/inf-incomplete-2.toml b/internal/toml-test/tests/invalid/float/inf-incomplete-2.toml
new file mode 100644
index 0000000..afda712
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/inf-incomplete-2.toml
@@ -0,0 +1 @@
+inf-incomplete-2 = +in
diff --git a/internal/toml-test/tests/invalid/float/inf-incomplete-3.toml b/internal/toml-test/tests/invalid/float/inf-incomplete-3.toml
new file mode 100644
index 0000000..bd82738
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/inf-incomplete-3.toml
@@ -0,0 +1 @@
+inf-incomplete-3 = -in
diff --git a/internal/toml-test/tests/invalid/float/inf_underscore.toml b/internal/toml-test/tests/invalid/float/inf_underscore.toml
new file mode 100644
index 0000000..8e6ab17
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/inf_underscore.toml
@@ -0,0 +1 @@
+inf_underscore = in_f
diff --git a/internal/toml-test/tests/invalid/float/leading-point-neg.toml b/internal/toml-test/tests/invalid/float/leading-point-neg.toml
new file mode 100644
index 0000000..74c526f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-point-neg.toml
@@ -0,0 +1 @@
+leading-point-neg = -.12345
diff --git a/internal/toml-test/tests/invalid/float/leading-point-plus.toml b/internal/toml-test/tests/invalid/float/leading-point-plus.toml
new file mode 100644
index 0000000..85e23a2
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-point-plus.toml
@@ -0,0 +1 @@
+leading-point-plus = +.12345
diff --git a/internal/toml-test/tests/invalid/float/leading-point.toml b/internal/toml-test/tests/invalid/float/leading-point.toml
new file mode 100644
index 0000000..a3c29b9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-point.toml
@@ -0,0 +1 @@
+leading-point = .12345
diff --git a/internal/toml-test/tests/invalid/float/leading-us.toml b/internal/toml-test/tests/invalid/float/leading-us.toml
new file mode 100644
index 0000000..fa94fd0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-us.toml
@@ -0,0 +1 @@
+leading-us = _1.2
diff --git a/internal/toml-test/tests/invalid/float/leading-zero-neg.toml b/internal/toml-test/tests/invalid/float/leading-zero-neg.toml
new file mode 100644
index 0000000..94a4e70
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-zero-neg.toml
@@ -0,0 +1 @@
+leading-zero-neg = -03.14
diff --git a/internal/toml-test/tests/invalid/float/leading-zero-plus.toml b/internal/toml-test/tests/invalid/float/leading-zero-plus.toml
new file mode 100644
index 0000000..12bfd60
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-zero-plus.toml
@@ -0,0 +1 @@
+leading-zero-plus = +03.14
diff --git a/internal/toml-test/tests/invalid/float/leading-zero.toml b/internal/toml-test/tests/invalid/float/leading-zero.toml
new file mode 100644
index 0000000..551fb25
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/leading-zero.toml
@@ -0,0 +1 @@
+leading-zero = 03.14
diff --git a/internal/toml-test/tests/invalid/float/nan-capital.toml b/internal/toml-test/tests/invalid/float/nan-capital.toml
new file mode 100644
index 0000000..f6f926e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/nan-capital.toml
@@ -0,0 +1 @@
+v = NaN
diff --git a/internal/toml-test/tests/invalid/float/nan-incomplete-1.toml b/internal/toml-test/tests/invalid/float/nan-incomplete-1.toml
new file mode 100644
index 0000000..df2e261
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/nan-incomplete-1.toml
@@ -0,0 +1 @@
+nan-incomplete-1 = na
diff --git a/internal/toml-test/tests/invalid/float/nan-incomplete-2.toml b/internal/toml-test/tests/invalid/float/nan-incomplete-2.toml
new file mode 100644
index 0000000..6d2e8fb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/nan-incomplete-2.toml
@@ -0,0 +1 @@
+nan-incomplete-2 = +na
diff --git a/internal/toml-test/tests/invalid/float/nan-incomplete-3.toml b/internal/toml-test/tests/invalid/float/nan-incomplete-3.toml
new file mode 100644
index 0000000..9d365ac
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/nan-incomplete-3.toml
@@ -0,0 +1 @@
+nan-incomplete-3 = -na
diff --git a/internal/toml-test/tests/invalid/float/nan_underscore.toml b/internal/toml-test/tests/invalid/float/nan_underscore.toml
new file mode 100644
index 0000000..44c8bf1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/nan_underscore.toml
@@ -0,0 +1 @@
+nan_underscore = na_n
diff --git a/internal/toml-test/tests/invalid/float/trailing-point-min.toml b/internal/toml-test/tests/invalid/float/trailing-point-min.toml
new file mode 100644
index 0000000..48741e3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/trailing-point-min.toml
@@ -0,0 +1 @@
+trailing-point-min = -1.
diff --git a/internal/toml-test/tests/invalid/float/trailing-point-plus.toml b/internal/toml-test/tests/invalid/float/trailing-point-plus.toml
new file mode 100644
index 0000000..9926039
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/trailing-point-plus.toml
@@ -0,0 +1 @@
+trailing-point-plus = +1.
diff --git a/internal/toml-test/tests/invalid/float/trailing-point.toml b/internal/toml-test/tests/invalid/float/trailing-point.toml
new file mode 100644
index 0000000..76c95d9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/trailing-point.toml
@@ -0,0 +1 @@
+trailing-point = 1.
diff --git a/internal/toml-test/tests/invalid/float/trailing-us-exp.toml b/internal/toml-test/tests/invalid/float/trailing-us-exp.toml
new file mode 100644
index 0000000..7bba971
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/trailing-us-exp.toml
@@ -0,0 +1,4 @@
+# trailing underscore in integer part is not allowed
+trailing-us-exp = 1_e2
+# trailing underscore in float part is not allowed
+trailing-us-exp2 = 1.2_e2
diff --git a/internal/toml-test/tests/invalid/float/trailing-us.toml b/internal/toml-test/tests/invalid/float/trailing-us.toml
new file mode 100644
index 0000000..ddfb7ba
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/trailing-us.toml
@@ -0,0 +1 @@
+trailing-us = 1.2_
diff --git a/internal/toml-test/tests/invalid/float/us-after-point.toml b/internal/toml-test/tests/invalid/float/us-after-point.toml
new file mode 100644
index 0000000..7f15f57
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/us-after-point.toml
@@ -0,0 +1 @@
+us-after-point = 1._2
diff --git a/internal/toml-test/tests/invalid/float/us-before-point.toml b/internal/toml-test/tests/invalid/float/us-before-point.toml
new file mode 100644
index 0000000..c8edcbf
--- /dev/null
+++ b/internal/toml-test/tests/invalid/float/us-before-point.toml
@@ -0,0 +1 @@
+us-before-point = 1_.2
diff --git a/internal/toml-test/tests/invalid/inline-table/add.toml b/internal/toml-test/tests/invalid/inline-table/add.toml
new file mode 100644
index 0000000..bb8b3e8
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/add.toml
@@ -0,0 +1,3 @@
+a={}
+# Inline tables are immutable and can't be extended
+[a.b]
diff --git a/internal/toml-test/tests/invalid/inline-table/bad-key-syntax.toml b/internal/toml-test/tests/invalid/inline-table/bad-key-syntax.toml
new file mode 100644
index 0000000..bf71827
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/bad-key-syntax.toml
@@ -0,0 +1 @@
+tbl = { a = 1, [b] }
diff --git a/internal/toml-test/tests/invalid/inline-table/dotted-key-conflict.toml b/internal/toml-test/tests/invalid/inline-table/dotted-key-conflict.toml
new file mode 100644
index 0000000..58fa3b2
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/dotted-key-conflict.toml
@@ -0,0 +1 @@
+tbl = { a.b = "a_b", a.b.c = "a_b_c" }
diff --git a/internal/toml-test/tests/invalid/inline-table/double-comma.toml b/internal/toml-test/tests/invalid/inline-table/double-comma.toml
new file mode 100644
index 0000000..2d2913a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/double-comma.toml
@@ -0,0 +1 @@
+t = {x=3,,y=4}
diff --git a/internal/toml-test/tests/invalid/inline-table/duplicate-key.toml b/internal/toml-test/tests/invalid/inline-table/duplicate-key.toml
new file mode 100644
index 0000000..0789e14
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/duplicate-key.toml
@@ -0,0 +1,2 @@
+# Duplicate keys within an inline table are invalid
+a={b=1, b=2}
diff --git a/internal/toml-test/tests/invalid/inline-table/empty.toml b/internal/toml-test/tests/invalid/inline-table/empty.toml
new file mode 100644
index 0000000..ebbb066
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/empty.toml
@@ -0,0 +1 @@
+t = {,}
diff --git a/internal/toml-test/tests/invalid/inline-table/linebreak-1.toml b/internal/toml-test/tests/invalid/inline-table/linebreak-1.toml
new file mode 100644
index 0000000..0ae44b6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/linebreak-1.toml
@@ -0,0 +1,4 @@
+# No newlines are allowed between the curly braces unless they are valid within
+# a value.
+simple = { a = 1
+}
diff --git a/internal/toml-test/tests/invalid/inline-table/linebreak-2.toml b/internal/toml-test/tests/invalid/inline-table/linebreak-2.toml
new file mode 100644
index 0000000..5ea4eaf
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/linebreak-2.toml
@@ -0,0 +1,2 @@
+t = {a=1,
+b=2}
diff --git a/internal/toml-test/tests/invalid/inline-table/linebreak-3.toml b/internal/toml-test/tests/invalid/inline-table/linebreak-3.toml
new file mode 100644
index 0000000..7f6e892
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/linebreak-3.toml
@@ -0,0 +1,2 @@
+t = {a=1
+,b=2}
diff --git a/internal/toml-test/tests/invalid/inline-table/linebreak-4.toml b/internal/toml-test/tests/invalid/inline-table/linebreak-4.toml
new file mode 100644
index 0000000..3f34e15
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/linebreak-4.toml
@@ -0,0 +1,4 @@
+json_like = {
+ first = "Tom",
+ last = "Preston-Werner"
+}
diff --git a/internal/toml-test/tests/invalid/inline-table/nested_key_conflict.toml b/internal/toml-test/tests/invalid/inline-table/nested_key_conflict.toml
new file mode 100644
index 0000000..c567c3f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/nested_key_conflict.toml
@@ -0,0 +1,2 @@
+tbl = { fruit = { apple.color = "red" }, fruit.apple.texture = { smooth = true } }
+
diff --git a/internal/toml-test/tests/invalid/inline-table/no-comma.toml b/internal/toml-test/tests/invalid/inline-table/no-comma.toml
new file mode 100644
index 0000000..0a4083d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/no-comma.toml
@@ -0,0 +1 @@
+t = {x = 3 y = 4}
diff --git a/internal/toml-test/tests/invalid/inline-table/overwrite.toml b/internal/toml-test/tests/invalid/inline-table/overwrite.toml
new file mode 100644
index 0000000..4da0a85
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/overwrite.toml
@@ -0,0 +1,3 @@
+a.b=0
+# Since table "a" is already defined, it can't be replaced by an inline table.
+a={}
diff --git a/internal/toml-test/tests/invalid/inline-table/trailing-comma.toml b/internal/toml-test/tests/invalid/inline-table/trailing-comma.toml
new file mode 100644
index 0000000..6b67e02
--- /dev/null
+++ b/internal/toml-test/tests/invalid/inline-table/trailing-comma.toml
@@ -0,0 +1,3 @@
+# A terminating comma (also called trailing comma) is not permitted after the
+# last key/value pair in an inline table
+abc = { abc = 123, }
diff --git a/internal/toml-test/tests/invalid/integer/capital-bin.toml b/internal/toml-test/tests/invalid/integer/capital-bin.toml
new file mode 100644
index 0000000..bea4e5e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/capital-bin.toml
@@ -0,0 +1 @@
+capital-bin = 0B0
diff --git a/internal/toml-test/tests/invalid/integer/capital-hex.toml b/internal/toml-test/tests/invalid/integer/capital-hex.toml
new file mode 100644
index 0000000..bc064bb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/capital-hex.toml
@@ -0,0 +1 @@
+capital-hex = 0X1
diff --git a/internal/toml-test/tests/invalid/integer/capital-oct.toml b/internal/toml-test/tests/invalid/integer/capital-oct.toml
new file mode 100644
index 0000000..ea08a4d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/capital-oct.toml
@@ -0,0 +1 @@
+capital-oct = 0O0
diff --git a/internal/toml-test/tests/invalid/integer/double-sign-nex.toml b/internal/toml-test/tests/invalid/integer/double-sign-nex.toml
new file mode 100644
index 0000000..ff014cf
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/double-sign-nex.toml
@@ -0,0 +1 @@
+double-sign-nex = --99
diff --git a/internal/toml-test/tests/invalid/integer/double-sign-plus.toml b/internal/toml-test/tests/invalid/integer/double-sign-plus.toml
new file mode 100644
index 0000000..845ebb5
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/double-sign-plus.toml
@@ -0,0 +1 @@
+double-sign-plus = ++99
diff --git a/internal/toml-test/tests/invalid/integer/double-us.toml b/internal/toml-test/tests/invalid/integer/double-us.toml
new file mode 100644
index 0000000..cd00561
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/double-us.toml
@@ -0,0 +1 @@
+double-us = 1__23
diff --git a/internal/toml-test/tests/invalid/integer/incomplete-bin.toml b/internal/toml-test/tests/invalid/integer/incomplete-bin.toml
new file mode 100644
index 0000000..2d80143
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/incomplete-bin.toml
@@ -0,0 +1 @@
+incomplete-bin = 0b
diff --git a/internal/toml-test/tests/invalid/integer/incomplete-hex.toml b/internal/toml-test/tests/invalid/integer/incomplete-hex.toml
new file mode 100644
index 0000000..691b09c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/incomplete-hex.toml
@@ -0,0 +1 @@
+incomplete-hex = 0x
diff --git a/internal/toml-test/tests/invalid/integer/incomplete-oct.toml b/internal/toml-test/tests/invalid/integer/incomplete-oct.toml
new file mode 100644
index 0000000..a248b71
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/incomplete-oct.toml
@@ -0,0 +1 @@
+incomplete-oct = 0o
diff --git a/internal/toml-test/tests/invalid/integer/integer.multi b/internal/toml-test/tests/invalid/integer/integer.multi
new file mode 100644
index 0000000..40ac409
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/integer.multi
@@ -0,0 +1,41 @@
+leading-zero-1 = 01
+leading-zero-2 = 00
+leading-zero-3 = 0_0
+leading-zero-sign-1 = -01
+leading-zero-sign-2 = +01
+leading-zero-sign-3 = +0_1
+
+double-sign-plus = ++99
+double-sign-nex = --99
+
+negative-hex = -0xff
+negative-bin = -0b11010110
+negative-oct = -0o99
+
+positive-hex = +0xff
+positive-bin = +0b11010110
+positive-oct = +0o99
+
+trailing-us = 123_
+leading-us = _123
+double-us = 1__23
+
+us-after-hex = 0x_1
+us-after-oct = 0o_1
+us-after-bin = 0b_1
+
+trailing-us-hex = 0x1_
+trailing-us-oct = 0o1_
+trailing-us-bin = 0b1_
+
+leading-us-hex = _0o1
+leading-us-oct = _0o1
+leading-us-bin = _0o1
+
+invalid-hex = 0xaafz
+invalid-oct = 0o778
+invalid-bin = 0b0012
+
+capital-hex = 0X1
+capital-oct = 0O0
+capital-bin = 0B0
diff --git a/internal/toml-test/tests/invalid/integer/invalid-bin.toml b/internal/toml-test/tests/invalid/integer/invalid-bin.toml
new file mode 100644
index 0000000..d018258
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/invalid-bin.toml
@@ -0,0 +1 @@
+invalid-bin = 0b0012
diff --git a/internal/toml-test/tests/invalid/integer/invalid-hex.toml b/internal/toml-test/tests/invalid/integer/invalid-hex.toml
new file mode 100644
index 0000000..7cc0e85
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/invalid-hex.toml
@@ -0,0 +1 @@
+invalid-hex = 0xaafz
diff --git a/internal/toml-test/tests/invalid/integer/invalid-oct.toml b/internal/toml-test/tests/invalid/integer/invalid-oct.toml
new file mode 100644
index 0000000..03181b1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/invalid-oct.toml
@@ -0,0 +1 @@
+invalid-oct = 0o778
diff --git a/internal/toml-test/tests/invalid/integer/leading-us-bin.toml b/internal/toml-test/tests/invalid/integer/leading-us-bin.toml
new file mode 100644
index 0000000..8885c03
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-us-bin.toml
@@ -0,0 +1 @@
+leading-us-bin = _0o1
diff --git a/internal/toml-test/tests/invalid/integer/leading-us-hex.toml b/internal/toml-test/tests/invalid/integer/leading-us-hex.toml
new file mode 100644
index 0000000..9f193f8
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-us-hex.toml
@@ -0,0 +1 @@
+leading-us-hex = _0o1
diff --git a/internal/toml-test/tests/invalid/integer/leading-us-oct.toml b/internal/toml-test/tests/invalid/integer/leading-us-oct.toml
new file mode 100644
index 0000000..a717f1c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-us-oct.toml
@@ -0,0 +1 @@
+leading-us-oct = _0o1
diff --git a/internal/toml-test/tests/invalid/integer/leading-us.toml b/internal/toml-test/tests/invalid/integer/leading-us.toml
new file mode 100644
index 0000000..d059f50
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-us.toml
@@ -0,0 +1 @@
+leading-us = _123
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-1.toml b/internal/toml-test/tests/invalid/integer/leading-zero-1.toml
new file mode 100644
index 0000000..eba6bda
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-1.toml
@@ -0,0 +1 @@
+leading-zero-1 = 01
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-2.toml b/internal/toml-test/tests/invalid/integer/leading-zero-2.toml
new file mode 100644
index 0000000..436af56
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-2.toml
@@ -0,0 +1 @@
+leading-zero-2 = 00
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-3.toml b/internal/toml-test/tests/invalid/integer/leading-zero-3.toml
new file mode 100644
index 0000000..5404fc8
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-3.toml
@@ -0,0 +1 @@
+leading-zero-3 = 0_0
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-sign-1.toml b/internal/toml-test/tests/invalid/integer/leading-zero-sign-1.toml
new file mode 100644
index 0000000..1f15a9d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-sign-1.toml
@@ -0,0 +1 @@
+leading-zero-sign-1 = -01
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-sign-2.toml b/internal/toml-test/tests/invalid/integer/leading-zero-sign-2.toml
new file mode 100644
index 0000000..fa259c3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-sign-2.toml
@@ -0,0 +1 @@
+leading-zero-sign-2 = +01
diff --git a/internal/toml-test/tests/invalid/integer/leading-zero-sign-3.toml b/internal/toml-test/tests/invalid/integer/leading-zero-sign-3.toml
new file mode 100644
index 0000000..3ac64a5
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/leading-zero-sign-3.toml
@@ -0,0 +1 @@
+leading-zero-sign-3 = +0_1
diff --git a/internal/toml-test/tests/invalid/integer/negative-bin.toml b/internal/toml-test/tests/invalid/integer/negative-bin.toml
new file mode 100644
index 0000000..948b236
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/negative-bin.toml
@@ -0,0 +1 @@
+negative-bin = -0b11010110
diff --git a/internal/toml-test/tests/invalid/integer/negative-hex.toml b/internal/toml-test/tests/invalid/integer/negative-hex.toml
new file mode 100644
index 0000000..67c31ea
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/negative-hex.toml
@@ -0,0 +1 @@
+negative-hex = -0xff
diff --git a/internal/toml-test/tests/invalid/integer/negative-oct.toml b/internal/toml-test/tests/invalid/integer/negative-oct.toml
new file mode 100644
index 0000000..b0c5ab0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/negative-oct.toml
@@ -0,0 +1 @@
+negative-oct = -0o99
diff --git a/internal/toml-test/tests/invalid/integer/positive-bin.toml b/internal/toml-test/tests/invalid/integer/positive-bin.toml
new file mode 100644
index 0000000..7d6de65
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/positive-bin.toml
@@ -0,0 +1 @@
+positive-bin = +0b11010110
diff --git a/internal/toml-test/tests/invalid/integer/positive-hex.toml b/internal/toml-test/tests/invalid/integer/positive-hex.toml
new file mode 100644
index 0000000..bd503aa
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/positive-hex.toml
@@ -0,0 +1 @@
+positive-hex = +0xff
diff --git a/internal/toml-test/tests/invalid/integer/positive-oct.toml b/internal/toml-test/tests/invalid/integer/positive-oct.toml
new file mode 100644
index 0000000..d77aa52
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/positive-oct.toml
@@ -0,0 +1 @@
+positive-oct = +0o99
diff --git a/internal/toml-test/tests/invalid/integer/text-after-integer.toml b/internal/toml-test/tests/invalid/integer/text-after-integer.toml
new file mode 100644
index 0000000..42de7af
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/text-after-integer.toml
@@ -0,0 +1 @@
+answer = 42 the ultimate answer?
diff --git a/internal/toml-test/tests/invalid/integer/trailing-us-bin.toml b/internal/toml-test/tests/invalid/integer/trailing-us-bin.toml
new file mode 100644
index 0000000..9a0b574
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/trailing-us-bin.toml
@@ -0,0 +1 @@
+trailing-us-bin = 0b1_
diff --git a/internal/toml-test/tests/invalid/integer/trailing-us-hex.toml b/internal/toml-test/tests/invalid/integer/trailing-us-hex.toml
new file mode 100644
index 0000000..2a96455
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/trailing-us-hex.toml
@@ -0,0 +1 @@
+trailing-us-hex = 0x1_
diff --git a/internal/toml-test/tests/invalid/integer/trailing-us-oct.toml b/internal/toml-test/tests/invalid/integer/trailing-us-oct.toml
new file mode 100644
index 0000000..d28e8bc
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/trailing-us-oct.toml
@@ -0,0 +1 @@
+trailing-us-oct = 0o1_
diff --git a/internal/toml-test/tests/invalid/integer/trailing-us.toml b/internal/toml-test/tests/invalid/integer/trailing-us.toml
new file mode 100644
index 0000000..e11e8ff
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/trailing-us.toml
@@ -0,0 +1 @@
+trailing-us = 123_
diff --git a/internal/toml-test/tests/invalid/integer/us-after-bin.toml b/internal/toml-test/tests/invalid/integer/us-after-bin.toml
new file mode 100644
index 0000000..61dd895
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/us-after-bin.toml
@@ -0,0 +1 @@
+us-after-bin = 0b_1
diff --git a/internal/toml-test/tests/invalid/integer/us-after-hex.toml b/internal/toml-test/tests/invalid/integer/us-after-hex.toml
new file mode 100644
index 0000000..200220a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/us-after-hex.toml
@@ -0,0 +1 @@
+us-after-hex = 0x_1
diff --git a/internal/toml-test/tests/invalid/integer/us-after-oct.toml b/internal/toml-test/tests/invalid/integer/us-after-oct.toml
new file mode 100644
index 0000000..23f5300
--- /dev/null
+++ b/internal/toml-test/tests/invalid/integer/us-after-oct.toml
@@ -0,0 +1 @@
+us-after-oct = 0o_1
diff --git a/internal/toml-test/tests/invalid/key/after-array.toml b/internal/toml-test/tests/invalid/key/after-array.toml
new file mode 100644
index 0000000..5c1a1b0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/after-array.toml
@@ -0,0 +1 @@
+[[agencies]] owner = "S Cjelli"
diff --git a/internal/toml-test/tests/invalid/key/after-table.toml b/internal/toml-test/tests/invalid/key/after-table.toml
new file mode 100644
index 0000000..6886784
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/after-table.toml
@@ -0,0 +1 @@
+[error] this = "should not be here"
diff --git a/internal/toml-test/tests/invalid/key/after-value.toml b/internal/toml-test/tests/invalid/key/after-value.toml
new file mode 100644
index 0000000..e05c47c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/after-value.toml
@@ -0,0 +1 @@
+first = "Tom" last = "Preston-Werner" # INVALID
diff --git a/internal/toml-test/tests/invalid/key/bare-invalid-character.toml b/internal/toml-test/tests/invalid/key/bare-invalid-character.toml
new file mode 100644
index 0000000..e50dbe3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/bare-invalid-character.toml
@@ -0,0 +1 @@
+bare!key = 123
diff --git a/internal/toml-test/tests/invalid/key/dotted-redefine-table.toml b/internal/toml-test/tests/invalid/key/dotted-redefine-table.toml
new file mode 100644
index 0000000..55e25ac
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/dotted-redefine-table.toml
@@ -0,0 +1,4 @@
+# Defined a.b as int
+a.b = 1
+# Tries to access it as table: error
+a.b.c = 2
diff --git a/internal/toml-test/tests/invalid/key/duplicate-keys.toml b/internal/toml-test/tests/invalid/key/duplicate-keys.toml
new file mode 100644
index 0000000..9b5aee0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/duplicate-keys.toml
@@ -0,0 +1,2 @@
+dupe = false
+dupe = true
diff --git a/internal/toml-test/tests/invalid/key/duplicate.toml b/internal/toml-test/tests/invalid/key/duplicate.toml
new file mode 100644
index 0000000..7847bd4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/duplicate.toml
@@ -0,0 +1,3 @@
+# DO NOT DO THIS
+name = "Tom"
+name = "Pradyun"
diff --git a/internal/toml-test/tests/invalid/key/empty.toml b/internal/toml-test/tests/invalid/key/empty.toml
new file mode 100644
index 0000000..09f998f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/empty.toml
@@ -0,0 +1 @@
+ = 1
diff --git a/internal/toml-test/tests/invalid/key/escape.toml b/internal/toml-test/tests/invalid/key/escape.toml
new file mode 100644
index 0000000..cdb8f4d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/escape.toml
@@ -0,0 +1 @@
+\u00c0 = "latin capital letter A with grave"
diff --git a/internal/toml-test/tests/invalid/key/hash.toml b/internal/toml-test/tests/invalid/key/hash.toml
new file mode 100644
index 0000000..e321b1f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/hash.toml
@@ -0,0 +1 @@
+a# = 1
diff --git a/internal/toml-test/tests/invalid/key/multiline.toml b/internal/toml-test/tests/invalid/key/multiline.toml
new file mode 100644
index 0000000..31ff67e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/multiline.toml
@@ -0,0 +1,2 @@
+"""long
+key""" = 1
diff --git a/internal/toml-test/tests/invalid/key/newline.toml b/internal/toml-test/tests/invalid/key/newline.toml
new file mode 100644
index 0000000..24acaa1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/newline.toml
@@ -0,0 +1,2 @@
+barekey
+ = 123
diff --git a/internal/toml-test/tests/invalid/key/no-eol.toml b/internal/toml-test/tests/invalid/key/no-eol.toml
new file mode 100644
index 0000000..3c58eee
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/no-eol.toml
@@ -0,0 +1 @@
+a = 1 b = 2
diff --git a/internal/toml-test/tests/invalid/key/open-bracket.toml b/internal/toml-test/tests/invalid/key/open-bracket.toml
new file mode 100644
index 0000000..f0aeb16
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/open-bracket.toml
@@ -0,0 +1 @@
+[abc = 1
diff --git a/internal/toml-test/tests/invalid/key/partial-quoted.toml b/internal/toml-test/tests/invalid/key/partial-quoted.toml
new file mode 100644
index 0000000..e491a2f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/partial-quoted.toml
@@ -0,0 +1 @@
+partial"quoted" = 5
diff --git a/internal/toml-test/tests/invalid/key/quoted-unclosed-1.toml b/internal/toml-test/tests/invalid/key/quoted-unclosed-1.toml
new file mode 100644
index 0000000..4ea331a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/quoted-unclosed-1.toml
@@ -0,0 +1 @@
+"key = x
diff --git a/internal/toml-test/tests/invalid/key/quoted-unclosed-2.toml b/internal/toml-test/tests/invalid/key/quoted-unclosed-2.toml
new file mode 100644
index 0000000..d341d80
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/quoted-unclosed-2.toml
@@ -0,0 +1 @@
+"key
diff --git a/internal/toml-test/tests/invalid/key/single-open-bracket.toml b/internal/toml-test/tests/invalid/key/single-open-bracket.toml
new file mode 100644
index 0000000..558ed37
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/single-open-bracket.toml
@@ -0,0 +1 @@
+[
diff --git a/internal/toml-test/tests/invalid/key/space.toml b/internal/toml-test/tests/invalid/key/space.toml
new file mode 100644
index 0000000..7c22703
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/space.toml
@@ -0,0 +1 @@
+a b = 1
diff --git a/internal/toml-test/tests/invalid/key/special-character.toml b/internal/toml-test/tests/invalid/key/special-character.toml
new file mode 100644
index 0000000..bc181a5
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/special-character.toml
@@ -0,0 +1 @@
+μ = "greek small letter mu"
diff --git a/internal/toml-test/tests/invalid/key/start-bracket.toml b/internal/toml-test/tests/invalid/key/start-bracket.toml
new file mode 100644
index 0000000..e0597ae
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/start-bracket.toml
@@ -0,0 +1,3 @@
+[a]
+[xyz = 5
+[b]
diff --git a/internal/toml-test/tests/invalid/key/start-dot.toml b/internal/toml-test/tests/invalid/key/start-dot.toml
new file mode 100644
index 0000000..9967c7d
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/start-dot.toml
@@ -0,0 +1 @@
+.key = 1
diff --git a/internal/toml-test/tests/invalid/key/two-equals.toml b/internal/toml-test/tests/invalid/key/two-equals.toml
new file mode 100644
index 0000000..25a0378
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/two-equals.toml
@@ -0,0 +1 @@
+key= = 1
diff --git a/internal/toml-test/tests/invalid/key/two-equals2.toml b/internal/toml-test/tests/invalid/key/two-equals2.toml
new file mode 100644
index 0000000..c5f2de8
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/two-equals2.toml
@@ -0,0 +1 @@
+a==1
diff --git a/internal/toml-test/tests/invalid/key/two-equals3.toml b/internal/toml-test/tests/invalid/key/two-equals3.toml
new file mode 100644
index 0000000..eb20372
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/two-equals3.toml
@@ -0,0 +1 @@
+a=b=1
diff --git a/internal/toml-test/tests/invalid/key/without-value-1.toml b/internal/toml-test/tests/invalid/key/without-value-1.toml
new file mode 100644
index 0000000..06bfde4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/without-value-1.toml
@@ -0,0 +1 @@
+key
diff --git a/internal/toml-test/tests/invalid/key/without-value-2.toml b/internal/toml-test/tests/invalid/key/without-value-2.toml
new file mode 100644
index 0000000..ca6d7eb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/without-value-2.toml
@@ -0,0 +1 @@
+key =
diff --git a/internal/toml-test/tests/invalid/key/without-value-3.toml b/internal/toml-test/tests/invalid/key/without-value-3.toml
new file mode 100644
index 0000000..a9c06ef
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/without-value-3.toml
@@ -0,0 +1 @@
+"key"
diff --git a/internal/toml-test/tests/invalid/key/without-value-4.toml b/internal/toml-test/tests/invalid/key/without-value-4.toml
new file mode 100644
index 0000000..adf9d62
--- /dev/null
+++ b/internal/toml-test/tests/invalid/key/without-value-4.toml
@@ -0,0 +1 @@
+"key" =
diff --git a/internal/toml-test/tests/invalid/spec/inline-table-2-0.toml b/internal/toml-test/tests/invalid/spec/inline-table-2-0.toml
new file mode 100644
index 0000000..7689e14
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/inline-table-2-0.toml
@@ -0,0 +1,3 @@
+[product]
+type = { name = "Nail" }
+type.edible = false # INVALID
diff --git a/internal/toml-test/tests/invalid/spec/inline-table-3-0.toml b/internal/toml-test/tests/invalid/spec/inline-table-3-0.toml
new file mode 100644
index 0000000..d6937eb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/inline-table-3-0.toml
@@ -0,0 +1,3 @@
+[product]
+type.name = "Nail"
+type = { edible = false } # INVALID
diff --git a/internal/toml-test/tests/invalid/spec/key-value-pair-1.toml b/internal/toml-test/tests/invalid/spec/key-value-pair-1.toml
new file mode 100644
index 0000000..56f085a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/key-value-pair-1.toml
@@ -0,0 +1 @@
+key = # INVALID
diff --git a/internal/toml-test/tests/invalid/spec/keys-2.toml b/internal/toml-test/tests/invalid/spec/keys-2.toml
new file mode 100644
index 0000000..1547a5c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/keys-2.toml
@@ -0,0 +1,3 @@
+= "no key name" # INVALID
+"" = "blank" # VALID but discouraged
+'' = 'blank' # VALID but discouraged
diff --git a/internal/toml-test/tests/invalid/spec/string-4-0.toml b/internal/toml-test/tests/invalid/spec/string-4-0.toml
new file mode 100644
index 0000000..72071ea
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/string-4-0.toml
@@ -0,0 +1,7 @@
+str4 = """Here are two quotation marks: "". Simple enough."""
+str5 = """Here are three quotation marks: """.""" # INVALID
+str5 = """Here are three quotation marks: ""\"."""
+str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
+
+# "This," she said, "is just a pointless statement."
+str7 = """"This," she said, "is just a pointless statement.""""
diff --git a/internal/toml-test/tests/invalid/spec/string-7-0.toml b/internal/toml-test/tests/invalid/spec/string-7-0.toml
new file mode 100644
index 0000000..7b20aec
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/string-7-0.toml
@@ -0,0 +1,7 @@
+quot15 = '''Here are fifteen quotation marks: """""""""""""""'''
+
+apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
+apos15 = "Here are fifteen apostrophes: '''''''''''''''"
+
+# 'That,' she said, 'is still pointless.'
+str = ''''That,' she said, 'is still pointless.''''
diff --git a/internal/toml-test/tests/invalid/spec/table-9-0.toml b/internal/toml-test/tests/invalid/spec/table-9-0.toml
new file mode 100644
index 0000000..adc3841
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/table-9-0.toml
@@ -0,0 +1,9 @@
+[fruit]
+apple.color = "red"
+apple.taste.sweet = true
+
+[fruit.apple] # INVALID
+# [fruit.apple.taste] # INVALID
+
+[fruit.apple.texture] # you can add sub-tables
+smooth = true
diff --git a/internal/toml-test/tests/invalid/spec/table-9-1.toml b/internal/toml-test/tests/invalid/spec/table-9-1.toml
new file mode 100644
index 0000000..3b5644e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/spec/table-9-1.toml
@@ -0,0 +1,9 @@
+[fruit]
+apple.color = "red"
+apple.taste.sweet = true
+
+# [fruit.apple] # INVALID
+[fruit.apple.taste] # INVALID
+
+[fruit.apple.texture] # you can add sub-tables
+smooth = true
diff --git a/internal/toml-test/tests/invalid/string/bad-byte-escape.toml b/internal/toml-test/tests/invalid/string/bad-byte-escape.toml
new file mode 100644
index 0000000..4c7be59
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-byte-escape.toml
@@ -0,0 +1 @@
+naughty = "\xAg"
diff --git a/internal/toml-test/tests/invalid/string/bad-codepoint.toml b/internal/toml-test/tests/invalid/string/bad-codepoint.toml
new file mode 100644
index 0000000..592db75
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-codepoint.toml
@@ -0,0 +1 @@
+invalid-codepoint = "This string contains a non scalar unicode codepoint \uD801"
diff --git a/internal/toml-test/tests/invalid/string/bad-concat.toml b/internal/toml-test/tests/invalid/string/bad-concat.toml
new file mode 100644
index 0000000..fc6d3c9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-concat.toml
@@ -0,0 +1 @@
+no_concat = "first" "second"
diff --git a/internal/toml-test/tests/invalid/string/bad-escape-1.toml b/internal/toml-test/tests/invalid/string/bad-escape-1.toml
new file mode 100644
index 0000000..60acb0c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-escape-1.toml
@@ -0,0 +1 @@
+invalid-escape = "This string has a bad \a escape character."
diff --git a/internal/toml-test/tests/invalid/string/bad-escape-2.toml b/internal/toml-test/tests/invalid/string/bad-escape-2.toml
new file mode 100644
index 0000000..2a20d8c
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-escape-2.toml
@@ -0,0 +1,2 @@
+invalid-escape = "This string has a bad \ escape character."
+
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc-1.toml b/internal/toml-test/tests/invalid/string/bad-hex-esc-1.toml
new file mode 100644
index 0000000..199c961
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc-1.toml
@@ -0,0 +1 @@
+bad-hex-esc-1 = "\x0g"
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc-2.toml b/internal/toml-test/tests/invalid/string/bad-hex-esc-2.toml
new file mode 100644
index 0000000..3ff0765
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc-2.toml
@@ -0,0 +1 @@
+bad-hex-esc-2 = "\xG0"
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc-3.toml b/internal/toml-test/tests/invalid/string/bad-hex-esc-3.toml
new file mode 100644
index 0000000..5a1df54
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc-3.toml
@@ -0,0 +1 @@
+bad-hex-esc-3 = "\x"
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc-4.toml b/internal/toml-test/tests/invalid/string/bad-hex-esc-4.toml
new file mode 100644
index 0000000..4df871b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc-4.toml
@@ -0,0 +1 @@
+bad-hex-esc-4 = "\x 50"
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc-5.toml b/internal/toml-test/tests/invalid/string/bad-hex-esc-5.toml
new file mode 100644
index 0000000..379922f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc-5.toml
@@ -0,0 +1 @@
+bad-hex-esc-5 = "\x 50"
diff --git a/internal/toml-test/tests/invalid/string/bad-hex-esc.multi b/internal/toml-test/tests/invalid/string/bad-hex-esc.multi
new file mode 100644
index 0000000..5216ce0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-hex-esc.multi
@@ -0,0 +1,4 @@
+bad-hex-esc-1 = "\x0g"
+bad-hex-esc-2 = "\xG0"
+bad-hex-esc-3 = "\x"
+bad-hex-esc-4 = "\x 50"
diff --git a/internal/toml-test/tests/invalid/string/bad-multiline.toml b/internal/toml-test/tests/invalid/string/bad-multiline.toml
new file mode 100644
index 0000000..786046b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-multiline.toml
@@ -0,0 +1,2 @@
+multi = "first line
+second line"
diff --git a/internal/toml-test/tests/invalid/string/bad-slash-escape.toml b/internal/toml-test/tests/invalid/string/bad-slash-escape.toml
new file mode 100644
index 0000000..154abad
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-slash-escape.toml
@@ -0,0 +1 @@
+invalid-escape = "This string has a bad \/ escape character."
diff --git a/internal/toml-test/tests/invalid/string/bad-uni-esc-1.toml b/internal/toml-test/tests/invalid/string/bad-uni-esc-1.toml
new file mode 100644
index 0000000..9eae4ab
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-uni-esc-1.toml
@@ -0,0 +1 @@
+str = "val\ue"
diff --git a/internal/toml-test/tests/invalid/string/bad-uni-esc-2.toml b/internal/toml-test/tests/invalid/string/bad-uni-esc-2.toml
new file mode 100644
index 0000000..dce8b08
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-uni-esc-2.toml
@@ -0,0 +1 @@
+str = "val\Ux"
diff --git a/internal/toml-test/tests/invalid/string/bad-uni-esc-3.toml b/internal/toml-test/tests/invalid/string/bad-uni-esc-3.toml
new file mode 100644
index 0000000..95ea098
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-uni-esc-3.toml
@@ -0,0 +1,2 @@
+str = "val\U0000000"
+
diff --git a/internal/toml-test/tests/invalid/string/bad-uni-esc-4.toml b/internal/toml-test/tests/invalid/string/bad-uni-esc-4.toml
new file mode 100644
index 0000000..b90d0fb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-uni-esc-4.toml
@@ -0,0 +1 @@
+str = "val\U0000"
diff --git a/internal/toml-test/tests/invalid/string/bad-uni-esc-5.toml b/internal/toml-test/tests/invalid/string/bad-uni-esc-5.toml
new file mode 100644
index 0000000..448d3c6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/bad-uni-esc-5.toml
@@ -0,0 +1 @@
+str = "val\Ugggggggg"
diff --git a/internal/toml-test/tests/invalid/string/basic-byte-escapes.toml b/internal/toml-test/tests/invalid/string/basic-byte-escapes.toml
new file mode 100644
index 0000000..e94452a
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-byte-escapes.toml
@@ -0,0 +1 @@
+answer = "\x33"
diff --git a/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-1.toml b/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-1.toml
new file mode 100644
index 0000000..b27e203
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-1.toml
@@ -0,0 +1 @@
+a = """\UFFFFFFFF"""
diff --git a/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-2.toml b/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-2.toml
new file mode 100644
index 0000000..17a9361
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-multiline-out-of-range-unicode-escape-2.toml
@@ -0,0 +1 @@
+a = """\U00D80000"""
diff --git a/internal/toml-test/tests/invalid/string/basic-multiline-quotes.toml b/internal/toml-test/tests/invalid/string/basic-multiline-quotes.toml
new file mode 100644
index 0000000..02ebf13
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-multiline-quotes.toml
@@ -0,0 +1 @@
+str5 = """Here are three quotation marks: """."""
diff --git a/internal/toml-test/tests/invalid/string/basic-multiline-unknown-escape.toml b/internal/toml-test/tests/invalid/string/basic-multiline-unknown-escape.toml
new file mode 100644
index 0000000..35c5cc5
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-multiline-unknown-escape.toml
@@ -0,0 +1 @@
+a = """\@"""
diff --git a/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-1.toml b/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-1.toml
new file mode 100644
index 0000000..ada1f55
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-1.toml
@@ -0,0 +1 @@
+a = "\UFFFFFFFF"
diff --git a/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-2.toml b/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-2.toml
new file mode 100644
index 0000000..d4833b3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-out-of-range-unicode-escape-2.toml
@@ -0,0 +1 @@
+a = "\U00D80000"
diff --git a/internal/toml-test/tests/invalid/string/basic-unknown-escape.toml b/internal/toml-test/tests/invalid/string/basic-unknown-escape.toml
new file mode 100644
index 0000000..381dd85
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/basic-unknown-escape.toml
@@ -0,0 +1 @@
+a = "\@"
diff --git a/internal/toml-test/tests/invalid/string/literal-multiline-quotes-1.toml b/internal/toml-test/tests/invalid/string/literal-multiline-quotes-1.toml
new file mode 100644
index 0000000..9cfb763
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/literal-multiline-quotes-1.toml
@@ -0,0 +1,2 @@
+a = '''6 apostrophes: ''''''
+
diff --git a/internal/toml-test/tests/invalid/string/literal-multiline-quotes-2.toml b/internal/toml-test/tests/invalid/string/literal-multiline-quotes-2.toml
new file mode 100644
index 0000000..9924236
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/literal-multiline-quotes-2.toml
@@ -0,0 +1 @@
+a = '''15 apostrophes: ''''''''''''''''''
diff --git a/internal/toml-test/tests/invalid/string/missing-quotes.toml b/internal/toml-test/tests/invalid/string/missing-quotes.toml
new file mode 100644
index 0000000..2292793
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/missing-quotes.toml
@@ -0,0 +1 @@
+name = value
diff --git a/internal/toml-test/tests/invalid/string/multiline-bad-escape-1.toml b/internal/toml-test/tests/invalid/string/multiline-bad-escape-1.toml
new file mode 100644
index 0000000..5de9e15
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-bad-escape-1.toml
@@ -0,0 +1,2 @@
+k = """t\a"""
+
diff --git a/internal/toml-test/tests/invalid/string/multiline-bad-escape-2.toml b/internal/toml-test/tests/invalid/string/multiline-bad-escape-2.toml
new file mode 100644
index 0000000..3d1ac10
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-bad-escape-2.toml
@@ -0,0 +1,2 @@
+# \<Space> is not a valid escape.
+k = """t\ t"""
diff --git a/internal/toml-test/tests/invalid/string/multiline-bad-escape-3.toml b/internal/toml-test/tests/invalid/string/multiline-bad-escape-3.toml
new file mode 100644
index 0000000..95621eb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-bad-escape-3.toml
@@ -0,0 +1,3 @@
+# \<Space> is not a valid escape.
+k = """t\ """
+
diff --git a/internal/toml-test/tests/invalid/string/multiline-escape-space.toml b/internal/toml-test/tests/invalid/string/multiline-escape-space.toml
new file mode 100644
index 0000000..13570d3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-escape-space.toml
@@ -0,0 +1,3 @@
+a = """
+ foo \ \n
+ bar"""
diff --git a/internal/toml-test/tests/invalid/string/multiline-no-close-2.toml b/internal/toml-test/tests/invalid/string/multiline-no-close-2.toml
new file mode 100644
index 0000000..f469402
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-no-close-2.toml
@@ -0,0 +1 @@
+x="""
diff --git a/internal/toml-test/tests/invalid/string/multiline-no-close.toml b/internal/toml-test/tests/invalid/string/multiline-no-close.toml
new file mode 100644
index 0000000..4ca9597
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-no-close.toml
@@ -0,0 +1,2 @@
+invalid = """
+ this will fail
diff --git a/internal/toml-test/tests/invalid/string/multiline-quotes-1.toml b/internal/toml-test/tests/invalid/string/multiline-quotes-1.toml
new file mode 100644
index 0000000..9f7a42b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/multiline-quotes-1.toml
@@ -0,0 +1 @@
+a = """6 quotes: """"""
diff --git a/internal/toml-test/tests/invalid/string/no-close.toml b/internal/toml-test/tests/invalid/string/no-close.toml
new file mode 100644
index 0000000..0c292fc
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/no-close.toml
@@ -0,0 +1 @@
+no-ending-quote = "One time, at band camp
diff --git a/internal/toml-test/tests/invalid/string/text-after-string.toml b/internal/toml-test/tests/invalid/string/text-after-string.toml
new file mode 100644
index 0000000..c92a6f1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/text-after-string.toml
@@ -0,0 +1 @@
+string = "Is there life after strings?" No.
diff --git a/internal/toml-test/tests/invalid/string/wrong-close.toml b/internal/toml-test/tests/invalid/string/wrong-close.toml
new file mode 100644
index 0000000..0f5c801
--- /dev/null
+++ b/internal/toml-test/tests/invalid/string/wrong-close.toml
@@ -0,0 +1 @@
+bad-ending-quote = "double and single'
diff --git a/internal/toml-test/tests/invalid/table/append-to-array-with-dotted-keys.toml b/internal/toml-test/tests/invalid/table/append-to-array-with-dotted-keys.toml
new file mode 100644
index 0000000..1cae3a9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/append-to-array-with-dotted-keys.toml
@@ -0,0 +1,4 @@
+[[a.b]]
+
+[a]
+b.y = 2
diff --git a/internal/toml-test/tests/invalid/table/append-with-dotted-keys-1.toml b/internal/toml-test/tests/invalid/table/append-with-dotted-keys-1.toml
new file mode 100644
index 0000000..0b5b4bb
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/append-with-dotted-keys-1.toml
@@ -0,0 +1,17 @@
+# First a.b.c defines a table: a.b.c = {z=9}
+#
+# Then we define a.b.c.t = "str" to add a str to the above table, making it:
+#
+# a.b.c = {z=9, t="..."}
+#
+# While this makes sense, logically, it was decided this is not valid TOML as
+# it's too confusing/convoluted.
+#
+# See: https://github.com/toml-lang/toml/issues/846
+# https://github.com/toml-lang/toml/pull/859
+
+[a.b.c]
+ z = 9
+
+[a]
+ b.c.t = "Using dotted keys to add to [a.b.c] after explicitly defining it above is not allowed"
diff --git a/internal/toml-test/tests/invalid/table/append-with-dotted-keys-2.toml b/internal/toml-test/tests/invalid/table/append-with-dotted-keys-2.toml
new file mode 100644
index 0000000..3f0a011
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/append-with-dotted-keys-2.toml
@@ -0,0 +1,8 @@
+# This is the same issue as in injection-1.toml, except that nests one level
+# deeper. See that file for a more complete description.
+
+[a.b.c.d]
+ z = 9
+
+[a]
+ b.c.d.k.t = "Using dotted keys to add to [a.b.c.d] after explicitly defining it above is not allowed"
diff --git a/internal/toml-test/tests/invalid/table/array-empty.toml b/internal/toml-test/tests/invalid/table/array-empty.toml
new file mode 100644
index 0000000..a470ca3
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/array-empty.toml
@@ -0,0 +1,2 @@
+[[]]
+name = "Born to Run"
diff --git a/internal/toml-test/tests/invalid/table/array-implicit.toml b/internal/toml-test/tests/invalid/table/array-implicit.toml
new file mode 100644
index 0000000..05f2507
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/array-implicit.toml
@@ -0,0 +1,14 @@
+# This test is a bit tricky. It should fail because the first use of
+# `[[albums.songs]]` without first declaring `albums` implies that `albums`
+# must be a table. The alternative would be quite weird. Namely, it wouldn't
+# comply with the TOML spec: "Each double-bracketed sub-table will belong to
+# the most *recently* defined table element *above* it."
+#
+# This is in contrast to the *valid* test, table-array-implicit where
+# `[[albums.songs]]` works by itself, so long as `[[albums]]` isn't declared
+# later. (Although, `[albums]` could be.)
+[[albums.songs]]
+name = "Glory Days"
+
+[[albums]]
+name = "Born in the USA"
diff --git a/internal/toml-test/tests/invalid/table/array-missing-bracket.toml b/internal/toml-test/tests/invalid/table/array-missing-bracket.toml
new file mode 100644
index 0000000..39c73b0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/array-missing-bracket.toml
@@ -0,0 +1,2 @@
+[[albums]
+name = "Born to Run"
diff --git a/internal/toml-test/tests/invalid/table/duplicate-key-dotted-array.toml b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-array.toml
new file mode 100644
index 0000000..b75c2bc
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-array.toml
@@ -0,0 +1,4 @@
+[fruit]
+apple.color = "red"
+
+[[fruit.apple]]
diff --git a/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table.toml b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table.toml
new file mode 100644
index 0000000..ade637b
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table.toml
@@ -0,0 +1,4 @@
+[fruit]
+apple.color = "red"
+
+[fruit.apple] # INVALID
diff --git a/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table2.toml b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table2.toml
new file mode 100644
index 0000000..609b660
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-key-dotted-table2.toml
@@ -0,0 +1,4 @@
+[fruit]
+apple.taste.sweet = true
+
+[fruit.apple.taste] # INVALID
diff --git a/internal/toml-test/tests/invalid/table/duplicate-key-table.toml b/internal/toml-test/tests/invalid/table/duplicate-key-table.toml
new file mode 100644
index 0000000..cedf05f
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-key-table.toml
@@ -0,0 +1,5 @@
+[fruit]
+type = "apple"
+
+[fruit.type]
+apple = "yes"
diff --git a/internal/toml-test/tests/invalid/table/duplicate-table-array.toml b/internal/toml-test/tests/invalid/table/duplicate-table-array.toml
new file mode 100644
index 0000000..a1d8705
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-table-array.toml
@@ -0,0 +1,2 @@
+[tbl]
+[[tbl]]
diff --git a/internal/toml-test/tests/invalid/table/duplicate-table-array2.toml b/internal/toml-test/tests/invalid/table/duplicate-table-array2.toml
new file mode 100644
index 0000000..72eacbd
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate-table-array2.toml
@@ -0,0 +1,2 @@
+[[tbl]]
+[tbl]
diff --git a/internal/toml-test/tests/invalid/table/duplicate.toml b/internal/toml-test/tests/invalid/table/duplicate.toml
new file mode 100644
index 0000000..73ef1cc
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/duplicate.toml
@@ -0,0 +1,5 @@
+[a]
+b = 1
+
+[a]
+c = 2
diff --git a/internal/toml-test/tests/invalid/table/empty-implicit-table.toml b/internal/toml-test/tests/invalid/table/empty-implicit-table.toml
new file mode 100644
index 0000000..0cc36d0
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/empty-implicit-table.toml
@@ -0,0 +1 @@
+[naughty..naughty]
diff --git a/internal/toml-test/tests/invalid/table/empty.toml b/internal/toml-test/tests/invalid/table/empty.toml
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/empty.toml
@@ -0,0 +1 @@
+[]
diff --git a/internal/toml-test/tests/invalid/table/equals-sign.toml b/internal/toml-test/tests/invalid/table/equals-sign.toml
new file mode 100644
index 0000000..eb5e742
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/equals-sign.toml
@@ -0,0 +1 @@
+[name=bad]
diff --git a/internal/toml-test/tests/invalid/table/llbrace.toml b/internal/toml-test/tests/invalid/table/llbrace.toml
new file mode 100644
index 0000000..047978e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/llbrace.toml
@@ -0,0 +1 @@
+[ [table]]
diff --git a/internal/toml-test/tests/invalid/table/nested-brackets-close.toml b/internal/toml-test/tests/invalid/table/nested-brackets-close.toml
new file mode 100644
index 0000000..c8b5a67
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/nested-brackets-close.toml
@@ -0,0 +1,2 @@
+[a]b]
+zyx = 42
diff --git a/internal/toml-test/tests/invalid/table/nested-brackets-open.toml b/internal/toml-test/tests/invalid/table/nested-brackets-open.toml
new file mode 100644
index 0000000..246d7e9
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/nested-brackets-open.toml
@@ -0,0 +1,2 @@
+[a[b]
+zyx = 42
diff --git a/internal/toml-test/tests/invalid/table/quoted-no-close.toml b/internal/toml-test/tests/invalid/table/quoted-no-close.toml
new file mode 100644
index 0000000..6e7c1a1
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/quoted-no-close.toml
@@ -0,0 +1,2 @@
+["where will it end]
+name = value
diff --git a/internal/toml-test/tests/invalid/table/redefine.toml b/internal/toml-test/tests/invalid/table/redefine.toml
new file mode 100644
index 0000000..fc23bf7
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/redefine.toml
@@ -0,0 +1,6 @@
+# Define b as int, and try to use it as a table: error
+[a]
+b = 1
+
+[a.b]
+c = 2
diff --git a/internal/toml-test/tests/invalid/table/rrbrace.toml b/internal/toml-test/tests/invalid/table/rrbrace.toml
new file mode 100644
index 0000000..3a4dee4
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/rrbrace.toml
@@ -0,0 +1 @@
+[[table] ]
diff --git a/internal/toml-test/tests/invalid/table/text-after-table.toml b/internal/toml-test/tests/invalid/table/text-after-table.toml
new file mode 100644
index 0000000..87da9db
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/text-after-table.toml
@@ -0,0 +1 @@
+[error] this shouldn't be here
diff --git a/internal/toml-test/tests/invalid/table/whitespace.toml b/internal/toml-test/tests/invalid/table/whitespace.toml
new file mode 100644
index 0000000..0a6a6a6
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/whitespace.toml
@@ -0,0 +1 @@
+[invalid key]
diff --git a/internal/toml-test/tests/invalid/table/with-pound.toml b/internal/toml-test/tests/invalid/table/with-pound.toml
new file mode 100644
index 0000000..e7b777e
--- /dev/null
+++ b/internal/toml-test/tests/invalid/table/with-pound.toml
@@ -0,0 +1,2 @@
+[key#group]
+answer = 42
diff --git a/internal/toml-test/tests/valid/array/array.json b/internal/toml-test/tests/valid/array/array.json
new file mode 100644
index 0000000..4e9d45b
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/array.json
@@ -0,0 +1,68 @@
+{
+ "comments": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ],
+ "dates": [
+ {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:00Z"
+ },
+ {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00Z"
+ },
+ {
+ "type": "datetime",
+ "value": "2006-06-01T11:00:00Z"
+ }
+ ],
+ "floats": [
+ {
+ "type": "float",
+ "value": "1.1"
+ },
+ {
+ "type": "float",
+ "value": "2.1"
+ },
+ {
+ "type": "float",
+ "value": "3.1"
+ }
+ ],
+ "ints": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ],
+ "strings": [
+ {
+ "type": "string",
+ "value": "a"
+ },
+ {
+ "type": "string",
+ "value": "b"
+ },
+ {
+ "type": "string",
+ "value": "c"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/array.toml b/internal/toml-test/tests/valid/array/array.toml
new file mode 100644
index 0000000..ab88465
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/array.toml
@@ -0,0 +1,12 @@
+ints = [1, 2, 3, ]
+floats = [1.1, 2.1, 3.1]
+strings = ["a", "b", "c"]
+dates = [
+ 1987-07-05T17:45:00Z,
+ 1979-05-27T07:32:00Z,
+ 2006-06-01T11:00:00Z,
+]
+comments = [
+ 1,
+ 2, #this is ok
+]
diff --git a/internal/toml-test/tests/valid/array/bool.json b/internal/toml-test/tests/valid/array/bool.json
new file mode 100644
index 0000000..7cede3f
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/bool.json
@@ -0,0 +1,12 @@
+{
+ "a": [
+ {
+ "type": "bool",
+ "value": "true"
+ },
+ {
+ "type": "bool",
+ "value": "false"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/bool.toml b/internal/toml-test/tests/valid/array/bool.toml
new file mode 100644
index 0000000..d2010c5
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/bool.toml
@@ -0,0 +1 @@
+a = [true, false]
diff --git a/internal/toml-test/tests/valid/array/empty.json b/internal/toml-test/tests/valid/array/empty.json
new file mode 100644
index 0000000..7b1ca0c
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/empty.json
@@ -0,0 +1,11 @@
+{
+ "thevoid": [
+ [
+ [
+ [
+ []
+ ]
+ ]
+ ]
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/empty.toml b/internal/toml-test/tests/valid/array/empty.toml
new file mode 100644
index 0000000..fa58dc6
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/empty.toml
@@ -0,0 +1 @@
+thevoid = [[[[[]]]]]
diff --git a/internal/toml-test/tests/valid/array/hetergeneous.json b/internal/toml-test/tests/valid/array/hetergeneous.json
new file mode 100644
index 0000000..751b624
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/hetergeneous.json
@@ -0,0 +1,34 @@
+{
+ "mixed": [
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ],
+ [
+ {
+ "type": "string",
+ "value": "a"
+ },
+ {
+ "type": "string",
+ "value": "b"
+ }
+ ],
+ [
+ {
+ "type": "float",
+ "value": "1.1"
+ },
+ {
+ "type": "float",
+ "value": "2.1"
+ }
+ ]
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/hetergeneous.toml b/internal/toml-test/tests/valid/array/hetergeneous.toml
new file mode 100644
index 0000000..a246fcf
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/hetergeneous.toml
@@ -0,0 +1 @@
+mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]
diff --git a/internal/toml-test/tests/valid/array/mixed-int-array.json b/internal/toml-test/tests/valid/array/mixed-int-array.json
new file mode 100644
index 0000000..2f3ac5e
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-array.json
@@ -0,0 +1,14 @@
+{
+ "arrays-and-ints": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ [
+ {
+ "type": "string",
+ "value": "Arrays are not integers."
+ }
+ ]
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/mixed-int-array.toml b/internal/toml-test/tests/valid/array/mixed-int-array.toml
new file mode 100644
index 0000000..051ec73
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-array.toml
@@ -0,0 +1 @@
+arrays-and-ints = [1, ["Arrays are not integers."]]
diff --git a/internal/toml-test/tests/valid/array/mixed-int-float.json b/internal/toml-test/tests/valid/array/mixed-int-float.json
new file mode 100644
index 0000000..e73059b
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-float.json
@@ -0,0 +1,12 @@
+{
+ "ints-and-floats": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "float",
+ "value": "1.1"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/mixed-int-float.toml b/internal/toml-test/tests/valid/array/mixed-int-float.toml
new file mode 100644
index 0000000..a5aa9b7
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-float.toml
@@ -0,0 +1 @@
+ints-and-floats = [1, 1.1]
diff --git a/internal/toml-test/tests/valid/array/mixed-int-string.json b/internal/toml-test/tests/valid/array/mixed-int-string.json
new file mode 100644
index 0000000..c6acc76
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-string.json
@@ -0,0 +1,12 @@
+{
+ "strings-and-ints": [
+ {
+ "type": "string",
+ "value": "hi"
+ },
+ {
+ "type": "integer",
+ "value": "42"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/mixed-int-string.toml b/internal/toml-test/tests/valid/array/mixed-int-string.toml
new file mode 100644
index 0000000..f348308
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-int-string.toml
@@ -0,0 +1 @@
+strings-and-ints = ["hi", 42]
diff --git a/internal/toml-test/tests/valid/array/mixed-string-table.json b/internal/toml-test/tests/valid/array/mixed-string-table.json
new file mode 100644
index 0000000..0de6a6e
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-string-table.json
@@ -0,0 +1,38 @@
+{
+ "contributors": [
+ {
+ "type": "string",
+ "value": "Foo Bar \u003cfoo@example.com\u003e"
+ },
+ {
+ "email": {
+ "type": "string",
+ "value": "bazqux@example.com"
+ },
+ "name": {
+ "type": "string",
+ "value": "Baz Qux"
+ },
+ "url": {
+ "type": "string",
+ "value": "https://example.com/bazqux"
+ }
+ }
+ ],
+ "mixed": [
+ {
+ "k": {
+ "type": "string",
+ "value": "a"
+ }
+ },
+ {
+ "type": "string",
+ "value": "b"
+ },
+ {
+ "type": "integer",
+ "value": "1"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/mixed-string-table.toml b/internal/toml-test/tests/valid/array/mixed-string-table.toml
new file mode 100644
index 0000000..f0ede9c
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/mixed-string-table.toml
@@ -0,0 +1,11 @@
+contributors = [
+ "Foo Bar <foo@example.com>",
+ { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
+]
+
+# Start with a table as the first element. This tests a case that some libraries
+# might have where they will check if the first entry is a table/map/hash/assoc
+# array and then encode it as a table array. This was a reasonable thing to do
+# before TOML 1.0 since arrays could only contain one type, but now it's no
+# longer.
+mixed = [{k="a"}, "b", 1]
diff --git a/internal/toml-test/tests/valid/array/nested-double.json b/internal/toml-test/tests/valid/array/nested-double.json
new file mode 100644
index 0000000..c89a8c9
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested-double.json
@@ -0,0 +1,28 @@
+{
+ "nest": [
+ [
+ [
+ {
+ "type": "string",
+ "value": "a"
+ }
+ ],
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ [
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ]
+ ]
+ ]
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/nested-double.toml b/internal/toml-test/tests/valid/array/nested-double.toml
new file mode 100644
index 0000000..c1de747
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested-double.toml
@@ -0,0 +1,6 @@
+nest = [
+ [
+ ["a"],
+ [1, 2, [3]]
+ ]
+]
diff --git a/internal/toml-test/tests/valid/array/nested-inline-table.json b/internal/toml-test/tests/valid/array/nested-inline-table.json
new file mode 100644
index 0000000..2ae5fa7
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested-inline-table.json
@@ -0,0 +1,7 @@
+{
+ "a": [
+ {
+ "b": {}
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/nested-inline-table.toml b/internal/toml-test/tests/valid/array/nested-inline-table.toml
new file mode 100644
index 0000000..e1e24f6
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested-inline-table.toml
@@ -0,0 +1 @@
+a = [ { b = {} } ]
diff --git a/internal/toml-test/tests/valid/array/nested.json b/internal/toml-test/tests/valid/array/nested.json
new file mode 100644
index 0000000..72c7589
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested.json
@@ -0,0 +1,16 @@
+{
+ "nest": [
+ [
+ {
+ "type": "string",
+ "value": "a"
+ }
+ ],
+ [
+ {
+ "type": "string",
+ "value": "b"
+ }
+ ]
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/nested.toml b/internal/toml-test/tests/valid/array/nested.toml
new file mode 100644
index 0000000..ce33022
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nested.toml
@@ -0,0 +1 @@
+nest = [["a"], ["b"]]
diff --git a/internal/toml-test/tests/valid/array/nospaces.json b/internal/toml-test/tests/valid/array/nospaces.json
new file mode 100644
index 0000000..6c89fce
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nospaces.json
@@ -0,0 +1,16 @@
+{
+ "ints": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/nospaces.toml b/internal/toml-test/tests/valid/array/nospaces.toml
new file mode 100644
index 0000000..6618936
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/nospaces.toml
@@ -0,0 +1 @@
+ints = [1,2,3]
diff --git a/internal/toml-test/tests/valid/array/string-quote-comma-2.json b/internal/toml-test/tests/valid/array/string-quote-comma-2.json
new file mode 100644
index 0000000..7cd5406
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-quote-comma-2.json
@@ -0,0 +1,8 @@
+{
+ "title": [
+ {
+ "type": "string",
+ "value": " \", "
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/string-quote-comma-2.toml b/internal/toml-test/tests/valid/array/string-quote-comma-2.toml
new file mode 100644
index 0000000..4758ddc
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-quote-comma-2.toml
@@ -0,0 +1 @@
+title = [ " \", ",]
diff --git a/internal/toml-test/tests/valid/array/string-quote-comma.json b/internal/toml-test/tests/valid/array/string-quote-comma.json
new file mode 100644
index 0000000..b78db42
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-quote-comma.json
@@ -0,0 +1,12 @@
+{
+ "title": [
+ {
+ "type": "string",
+ "value": "Client: \"XXXX\", Job: XXXX"
+ },
+ {
+ "type": "string",
+ "value": "Code: XXXX"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/string-quote-comma.toml b/internal/toml-test/tests/valid/array/string-quote-comma.toml
new file mode 100644
index 0000000..6b458e1
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-quote-comma.toml
@@ -0,0 +1,4 @@
+title = [
+"Client: \"XXXX\", Job: XXXX",
+"Code: XXXX"
+]
diff --git a/internal/toml-test/tests/valid/array/string-with-comma.json b/internal/toml-test/tests/valid/array/string-with-comma.json
new file mode 100644
index 0000000..42b77ef
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-with-comma.json
@@ -0,0 +1,12 @@
+{
+ "title": [
+ {
+ "type": "string",
+ "value": "Client: XXXX, Job: XXXX"
+ },
+ {
+ "type": "string",
+ "value": "Code: XXXX"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/string-with-comma.toml b/internal/toml-test/tests/valid/array/string-with-comma.toml
new file mode 100644
index 0000000..655c40e
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/string-with-comma.toml
@@ -0,0 +1,4 @@
+title = [
+"Client: XXXX, Job: XXXX",
+"Code: XXXX"
+]
diff --git a/internal/toml-test/tests/valid/array/strings.json b/internal/toml-test/tests/valid/array/strings.json
new file mode 100644
index 0000000..27617b4
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/strings.json
@@ -0,0 +1,20 @@
+{
+ "string_array": [
+ {
+ "type": "string",
+ "value": "all"
+ },
+ {
+ "type": "string",
+ "value": "strings"
+ },
+ {
+ "type": "string",
+ "value": "are the same"
+ },
+ {
+ "type": "string",
+ "value": "type"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/strings.toml b/internal/toml-test/tests/valid/array/strings.toml
new file mode 100644
index 0000000..51894ce
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/strings.toml
@@ -0,0 +1 @@
+string_array = [ "all", 'strings', """are the same""", '''type''']
diff --git a/internal/toml-test/tests/valid/array/table-array-string-backslash.json b/internal/toml-test/tests/valid/array/table-array-string-backslash.json
new file mode 100644
index 0000000..2c0feb1
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/table-array-string-backslash.json
@@ -0,0 +1,10 @@
+{
+ "foo": [
+ {
+ "bar": {
+ "type": "string",
+ "value": "\"{{baz}}\""
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/array/table-array-string-backslash.toml b/internal/toml-test/tests/valid/array/table-array-string-backslash.toml
new file mode 100644
index 0000000..f0de81e
--- /dev/null
+++ b/internal/toml-test/tests/valid/array/table-array-string-backslash.toml
@@ -0,0 +1 @@
+foo = [ { bar="\"{{baz}}\""} ]
diff --git a/internal/toml-test/tests/valid/bool/bool.json b/internal/toml-test/tests/valid/bool/bool.json
new file mode 100644
index 0000000..be47d6d
--- /dev/null
+++ b/internal/toml-test/tests/valid/bool/bool.json
@@ -0,0 +1,10 @@
+{
+ "f": {
+ "type": "bool",
+ "value": "false"
+ },
+ "t": {
+ "type": "bool",
+ "value": "true"
+ }
+}
diff --git a/internal/toml-test/tests/valid/bool/bool.toml b/internal/toml-test/tests/valid/bool/bool.toml
new file mode 100644
index 0000000..a8a829b
--- /dev/null
+++ b/internal/toml-test/tests/valid/bool/bool.toml
@@ -0,0 +1,2 @@
+t = true
+f = false
diff --git a/internal/toml-test/tests/valid/comment/at-eof.json b/internal/toml-test/tests/valid/comment/at-eof.json
new file mode 100644
index 0000000..af3154a
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/at-eof.json
@@ -0,0 +1,6 @@
+{
+ "key": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/comment/at-eof.toml b/internal/toml-test/tests/valid/comment/at-eof.toml
new file mode 100644
index 0000000..090b474
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/at-eof.toml
@@ -0,0 +1,2 @@
+# This is a full-line comment
+key = "value" # This is a comment at the end of a line
diff --git a/internal/toml-test/tests/valid/comment/at-eof2.json b/internal/toml-test/tests/valid/comment/at-eof2.json
new file mode 100644
index 0000000..af3154a
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/at-eof2.json
@@ -0,0 +1,6 @@
+{
+ "key": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/comment/at-eof2.toml b/internal/toml-test/tests/valid/comment/at-eof2.toml
new file mode 100644
index 0000000..090b474
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/at-eof2.toml
@@ -0,0 +1,2 @@
+# This is a full-line comment
+key = "value" # This is a comment at the end of a line
diff --git a/internal/toml-test/tests/valid/comment/everywhere.json b/internal/toml-test/tests/valid/comment/everywhere.json
new file mode 100644
index 0000000..6e714d1
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/everywhere.json
@@ -0,0 +1,26 @@
+{
+ "group": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ },
+ "dt": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:12-07:00"
+ },
+ "d": {
+ "type": "date-local",
+ "value": "1979-05-27"
+ },
+ "more": [
+ {
+ "type": "integer",
+ "value": "42"
+ },
+ {
+ "type": "integer",
+ "value": "42"
+ }
+ ]
+ }
+}
diff --git a/internal/toml-test/tests/valid/comment/everywhere.toml b/internal/toml-test/tests/valid/comment/everywhere.toml
new file mode 100644
index 0000000..a5da1d7
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/everywhere.toml
@@ -0,0 +1,28 @@
+# Top comment.
+ # Top comment.
+# Top comment.
+
+# [no-extraneous-groups-please]
+
+[group] # Comment
+answer = 42 # Comment
+# no-extraneous-keys-please = 999
+# Inbetween comment.
+more = [ # Comment
+ # What about multiple # comments?
+ # Can you handle it?
+ #
+ # Evil.
+# Evil.
+ 42, 42, # Comments within arrays are fun.
+ # What about multiple # comments?
+ # Can you handle it?
+ #
+ # Evil.
+# Evil.
+# ] Did I fool you?
+] # Hopefully not.
+
+# Make sure the space between the datetime and "#" isn't lexed.
+dt = 1979-05-27T07:32:12-07:00 # c
+d = 1979-05-27 # Comment
diff --git a/internal/toml-test/tests/valid/comment/noeol.json b/internal/toml-test/tests/valid/comment/noeol.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/noeol.json
@@ -0,0 +1 @@
+{}
diff --git a/internal/toml-test/tests/valid/comment/noeol.toml b/internal/toml-test/tests/valid/comment/noeol.toml
new file mode 100644
index 0000000..7662df8
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/noeol.toml
@@ -0,0 +1 @@
+# single comment without any eol characters \ No newline at end of file
diff --git a/internal/toml-test/tests/valid/comment/nonascii.json b/internal/toml-test/tests/valid/comment/nonascii.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/nonascii.json
@@ -0,0 +1 @@
+{}
diff --git a/internal/toml-test/tests/valid/comment/nonascii.toml b/internal/toml-test/tests/valid/comment/nonascii.toml
new file mode 100644
index 0000000..2fdde6b
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/nonascii.toml
@@ -0,0 +1 @@
+# ~ € ÿ ퟿  ￿ 𐀀 􏿿
diff --git a/internal/toml-test/tests/valid/comment/tricky.json b/internal/toml-test/tests/valid/comment/tricky.json
new file mode 100644
index 0000000..0994864
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/tricky.json
@@ -0,0 +1,98 @@
+{
+ "hash#tag": {
+ "#!": {
+ "type": "string",
+ "value": "hash bang"
+ },
+ "arr3": [
+ {
+ "type": "string",
+ "value": "#"
+ },
+ {
+ "type": "string",
+ "value": "#"
+ },
+ {
+ "type": "string",
+ "value": "###"
+ }
+ ],
+ "arr4": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ },
+ {
+ "type": "integer",
+ "value": "4"
+ }
+ ],
+ "arr5": [
+ [
+ [
+ [
+ [
+ {
+ "type": "string",
+ "value": "#"
+ }
+ ]
+ ]
+ ]
+ ]
+ ],
+ "tbl1": {
+ "#": {
+ "type": "string",
+ "value": "}#"
+ }
+ }
+ },
+ "section": {
+ "8": {
+ "type": "string",
+ "value": "eight"
+ },
+ "eleven": {
+ "type": "float",
+ "value": "11.1"
+ },
+ "five": {
+ "type": "float",
+ "value": "5.5"
+ },
+ "four": {
+ "type": "string",
+ "value": "# no comment\n# nor this\n#also not comment"
+ },
+ "one": {
+ "type": "string",
+ "value": "11"
+ },
+ "six": {
+ "type": "integer",
+ "value": "6"
+ },
+ "ten": {
+ "type": "float",
+ "value": "1000.0"
+ },
+ "three": {
+ "type": "string",
+ "value": "#"
+ },
+ "two": {
+ "type": "string",
+ "value": "22#"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/comment/tricky.toml b/internal/toml-test/tests/valid/comment/tricky.toml
new file mode 100644
index 0000000..c95f94d
--- /dev/null
+++ b/internal/toml-test/tests/valid/comment/tricky.toml
@@ -0,0 +1,31 @@
+[section]#attached comment
+#[notsection]
+one = "11"#cmt
+two = "22#"
+three = '#'
+
+four = """# no comment
+# nor this
+#also not comment"""#is_comment
+
+five = 5.5#66
+six = 6#7
+8 = "eight"
+#nine = 99
+ten = 10e2#1
+eleven = 1.11e1#23
+
+["hash#tag"]
+"#!" = "hash bang"
+arr3 = [ "#", '#', """###""" ]
+arr4 = [ 1,# 9, 9,
+2#,9
+,#9
+3#]
+,4]
+arr5 = [[[[#["#"],
+["#"]]]]#]
+]
+tbl1 = { "#" = '}#'}#}}
+
+
diff --git a/internal/toml-test/tests/valid/datetime/datetime.json b/internal/toml-test/tests/valid/datetime/datetime.json
new file mode 100644
index 0000000..5146335
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/datetime.json
@@ -0,0 +1,10 @@
+{
+ "lower": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:00Z"
+ },
+ "space": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:00Z"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/datetime.toml b/internal/toml-test/tests/valid/datetime/datetime.toml
new file mode 100644
index 0000000..653d521
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/datetime.toml
@@ -0,0 +1,2 @@
+space = 1987-07-05 17:45:00Z
+lower = 1987-07-05t17:45:00z
diff --git a/internal/toml-test/tests/valid/datetime/local-date.json b/internal/toml-test/tests/valid/datetime/local-date.json
new file mode 100644
index 0000000..8fea2fe
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local-date.json
@@ -0,0 +1,6 @@
+{
+ "bestdayever": {
+ "type": "date-local",
+ "value": "1987-07-05"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/local-date.toml b/internal/toml-test/tests/valid/datetime/local-date.toml
new file mode 100644
index 0000000..1be4a5f
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local-date.toml
@@ -0,0 +1 @@
+bestdayever = 1987-07-05
diff --git a/internal/toml-test/tests/valid/datetime/local-time.json b/internal/toml-test/tests/valid/datetime/local-time.json
new file mode 100644
index 0000000..5e527e9
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local-time.json
@@ -0,0 +1,10 @@
+{
+ "besttimeever": {
+ "type": "time-local",
+ "value": "17:45:00"
+ },
+ "milliseconds": {
+ "type": "time-local",
+ "value": "10:32:00.555"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/local-time.toml b/internal/toml-test/tests/valid/datetime/local-time.toml
new file mode 100644
index 0000000..6b07f6f
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local-time.toml
@@ -0,0 +1,2 @@
+besttimeever = 17:45:00
+milliseconds = 10:32:00.555
diff --git a/internal/toml-test/tests/valid/datetime/local.json b/internal/toml-test/tests/valid/datetime/local.json
new file mode 100644
index 0000000..12f8df5
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local.json
@@ -0,0 +1,14 @@
+{
+ "local": {
+ "type": "datetime-local",
+ "value": "1987-07-05T17:45:00"
+ },
+ "milli": {
+ "type": "datetime-local",
+ "value": "1977-12-21T10:32:00.555"
+ },
+ "space": {
+ "type": "datetime-local",
+ "value": "1987-07-05T17:45:00"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/local.toml b/internal/toml-test/tests/valid/datetime/local.toml
new file mode 100644
index 0000000..ab425ec
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/local.toml
@@ -0,0 +1,3 @@
+local = 1987-07-05T17:45:00
+milli = 1977-12-21T10:32:00.555
+space = 1987-07-05 17:45:00
diff --git a/internal/toml-test/tests/valid/datetime/milliseconds.json b/internal/toml-test/tests/valid/datetime/milliseconds.json
new file mode 100644
index 0000000..c49e400
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/milliseconds.json
@@ -0,0 +1,18 @@
+{
+ "utc1": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56.1234Z"
+ },
+ "utc2": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56.6000Z"
+ },
+ "wita1": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56.1234+08:00"
+ },
+ "wita2": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56.6000+08:00"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/milliseconds.toml b/internal/toml-test/tests/valid/datetime/milliseconds.toml
new file mode 100644
index 0000000..24b81f8
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/milliseconds.toml
@@ -0,0 +1,4 @@
+utc1 = 1987-07-05T17:45:56.1234Z
+utc2 = 1987-07-05T17:45:56.6Z
+wita1 = 1987-07-05T17:45:56.1234+08:00
+wita2 = 1987-07-05T17:45:56.6+08:00
diff --git a/internal/toml-test/tests/valid/datetime/no-seconds.json b/internal/toml-test/tests/valid/datetime/no-seconds.json
new file mode 100644
index 0000000..ba3652e
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/no-seconds.json
@@ -0,0 +1,18 @@
+{
+ "without-seconds-1": {
+ "type": "time-local",
+ "value": "13:37:00"
+ },
+ "without-seconds-2": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00Z"
+ },
+ "without-seconds-3": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00-07:00"
+ },
+ "without-seconds-4": {
+ "type": "datetime-local",
+ "value": "1979-05-27T07:32:00"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/no-seconds.toml b/internal/toml-test/tests/valid/datetime/no-seconds.toml
new file mode 100644
index 0000000..a0fd260
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/no-seconds.toml
@@ -0,0 +1,5 @@
+# Seconds are optional in date-time and time.
+without-seconds-1 = 13:37
+without-seconds-2 = 1979-05-27 07:32Z
+without-seconds-3 = 1979-05-27 07:32-07:00
+without-seconds-4 = 1979-05-27T07:32
diff --git a/internal/toml-test/tests/valid/datetime/timezone.json b/internal/toml-test/tests/valid/datetime/timezone.json
new file mode 100644
index 0000000..8dc8328
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/timezone.json
@@ -0,0 +1,18 @@
+{
+ "nzdt": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56+13:00"
+ },
+ "nzst": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56+12:00"
+ },
+ "pdt": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56-05:00"
+ },
+ "utc": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:56Z"
+ }
+}
diff --git a/internal/toml-test/tests/valid/datetime/timezone.toml b/internal/toml-test/tests/valid/datetime/timezone.toml
new file mode 100644
index 0000000..20f3835
--- /dev/null
+++ b/internal/toml-test/tests/valid/datetime/timezone.toml
@@ -0,0 +1,4 @@
+utc = 1987-07-05T17:45:56Z
+pdt = 1987-07-05T17:45:56-05:00
+nzst = 1987-07-05T17:45:56+12:00
+nzdt = 1987-07-05T17:45:56+13:00 # DST
diff --git a/internal/toml-test/tests/valid/empty-file.json b/internal/toml-test/tests/valid/empty-file.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/internal/toml-test/tests/valid/empty-file.json
@@ -0,0 +1 @@
+{}
diff --git a/internal/toml-test/tests/valid/empty-file.toml b/internal/toml-test/tests/valid/empty-file.toml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/internal/toml-test/tests/valid/empty-file.toml
diff --git a/internal/toml-test/tests/valid/example.json b/internal/toml-test/tests/valid/example.json
new file mode 100644
index 0000000..2ea95a2
--- /dev/null
+++ b/internal/toml-test/tests/valid/example.json
@@ -0,0 +1,26 @@
+{
+ "best-day-ever": {
+ "type": "datetime",
+ "value": "1987-07-05T17:45:00Z"
+ },
+ "numtheory": {
+ "boring": {
+ "type": "bool",
+ "value": "false"
+ },
+ "perfection": [
+ {
+ "type": "integer",
+ "value": "6"
+ },
+ {
+ "type": "integer",
+ "value": "28"
+ },
+ {
+ "type": "integer",
+ "value": "496"
+ }
+ ]
+ }
+}
diff --git a/internal/toml-test/tests/valid/example.toml b/internal/toml-test/tests/valid/example.toml
new file mode 100644
index 0000000..8cb02e0
--- /dev/null
+++ b/internal/toml-test/tests/valid/example.toml
@@ -0,0 +1,5 @@
+best-day-ever = 1987-07-05T17:45:00Z
+
+[numtheory]
+boring = false
+perfection = [6, 28, 496]
diff --git a/internal/toml-test/tests/valid/float/exponent.json b/internal/toml-test/tests/valid/float/exponent.json
new file mode 100644
index 0000000..e79a6e1
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/exponent.json
@@ -0,0 +1,34 @@
+{
+ "lower": {
+ "type": "float",
+ "value": "300.0"
+ },
+ "minustenth": {
+ "type": "float",
+ "value": "-0.1"
+ },
+ "neg": {
+ "type": "float",
+ "value": "0.03"
+ },
+ "pointlower": {
+ "type": "float",
+ "value": "310.0"
+ },
+ "pointupper": {
+ "type": "float",
+ "value": "310.0"
+ },
+ "pos": {
+ "type": "float",
+ "value": "300.0"
+ },
+ "upper": {
+ "type": "float",
+ "value": "300.0"
+ },
+ "zero": {
+ "type": "float",
+ "value": "3.0"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/exponent.toml b/internal/toml-test/tests/valid/float/exponent.toml
new file mode 100644
index 0000000..573e0a8
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/exponent.toml
@@ -0,0 +1,8 @@
+lower = 3e2
+upper = 3E2
+neg = 3e-2
+pos = 3E+2
+zero = 3e0
+pointlower = 3.1e2
+pointupper = 3.1E2
+minustenth = -1E-1
diff --git a/internal/toml-test/tests/valid/float/float.json b/internal/toml-test/tests/valid/float/float.json
new file mode 100644
index 0000000..f92d8d8
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/float.json
@@ -0,0 +1,18 @@
+{
+ "negpi": {
+ "type": "float",
+ "value": "-3.14"
+ },
+ "pi": {
+ "type": "float",
+ "value": "3.14"
+ },
+ "pospi": {
+ "type": "float",
+ "value": "3.14"
+ },
+ "zero-intpart": {
+ "type": "float",
+ "value": "0.123"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/float.toml b/internal/toml-test/tests/valid/float/float.toml
new file mode 100644
index 0000000..5f02322
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/float.toml
@@ -0,0 +1,4 @@
+pi = 3.14
+pospi = +3.14
+negpi = -3.14
+zero-intpart = 0.123
diff --git a/internal/toml-test/tests/valid/float/inf-and-nan.json b/internal/toml-test/tests/valid/float/inf-and-nan.json
new file mode 100644
index 0000000..e3f1ba3
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/inf-and-nan.json
@@ -0,0 +1,26 @@
+{
+ "infinity": {
+ "type": "float",
+ "value": "inf"
+ },
+ "infinity_neg": {
+ "type": "float",
+ "value": "-inf"
+ },
+ "infinity_plus": {
+ "type": "float",
+ "value": "+inf"
+ },
+ "nan": {
+ "type": "float",
+ "value": "nan"
+ },
+ "nan_neg": {
+ "type": "float",
+ "value": "nan"
+ },
+ "nan_plus": {
+ "type": "float",
+ "value": "nan"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/inf-and-nan.toml b/internal/toml-test/tests/valid/float/inf-and-nan.toml
new file mode 100644
index 0000000..c8f8459
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/inf-and-nan.toml
@@ -0,0 +1,8 @@
+# We don't encode +nan and -nan back with the signs; many languages don't
+# support a sign on NaN (it doesn't really make much sense).
+nan = nan
+nan_neg = -nan
+nan_plus = +nan
+infinity = inf
+infinity_neg = -inf
+infinity_plus = +inf
diff --git a/internal/toml-test/tests/valid/float/long.json b/internal/toml-test/tests/valid/float/long.json
new file mode 100644
index 0000000..73d7505
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/long.json
@@ -0,0 +1,10 @@
+{
+ "longpi": {
+ "type": "float",
+ "value": "3.141592653589793"
+ },
+ "neglongpi": {
+ "type": "float",
+ "value": "-3.141592653589793"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/long.toml b/internal/toml-test/tests/valid/float/long.toml
new file mode 100644
index 0000000..9558ae4
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/long.toml
@@ -0,0 +1,2 @@
+longpi = 3.141592653589793
+neglongpi = -3.141592653589793
diff --git a/internal/toml-test/tests/valid/float/underscore.json b/internal/toml-test/tests/valid/float/underscore.json
new file mode 100644
index 0000000..f54f7b5
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/underscore.json
@@ -0,0 +1,14 @@
+{
+ "after": {
+ "type": "float",
+ "value": "3141.5927"
+ },
+ "before": {
+ "type": "float",
+ "value": "3141.5927"
+ },
+ "exponent": {
+ "type": "float",
+ "value": "3.0e14"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/underscore.toml b/internal/toml-test/tests/valid/float/underscore.toml
new file mode 100644
index 0000000..343353a
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/underscore.toml
@@ -0,0 +1,3 @@
+before = 3_141.5927
+after = 3141.592_7
+exponent = 3e1_4
diff --git a/internal/toml-test/tests/valid/float/zero.json b/internal/toml-test/tests/valid/float/zero.json
new file mode 100644
index 0000000..61551e8
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/zero.json
@@ -0,0 +1,30 @@
+{
+ "zero": {
+ "type": "float",
+ "value": "0"
+ },
+ "signed-pos": {
+ "type": "float",
+ "value": "0"
+ },
+ "signed-neg": {
+ "type": "float",
+ "value": "0"
+ },
+ "exponent": {
+ "type": "float",
+ "value": "0"
+ },
+ "exponent-two-0": {
+ "type": "float",
+ "value": "0"
+ },
+ "exponent-signed-pos": {
+ "type": "float",
+ "value": "0"
+ },
+ "exponent-signed-neg": {
+ "type": "float",
+ "value": "0"
+ }
+}
diff --git a/internal/toml-test/tests/valid/float/zero.toml b/internal/toml-test/tests/valid/float/zero.toml
new file mode 100644
index 0000000..e7efb1f
--- /dev/null
+++ b/internal/toml-test/tests/valid/float/zero.toml
@@ -0,0 +1,7 @@
+zero = 0.0
+signed-pos = +0.0
+signed-neg = -0.0
+exponent = 0e0
+exponent-two-0 = 0e00
+exponent-signed-pos = +0e0
+exponent-signed-neg = -0e0
diff --git a/internal/toml-test/tests/valid/implicit-and-explicit-after.json b/internal/toml-test/tests/valid/implicit-and-explicit-after.json
new file mode 100644
index 0000000..663a34e
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-and-explicit-after.json
@@ -0,0 +1,16 @@
+{
+ "a": {
+ "b": {
+ "c": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ },
+ "better": {
+ "type": "integer",
+ "value": "43"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/implicit-and-explicit-after.toml b/internal/toml-test/tests/valid/implicit-and-explicit-after.toml
new file mode 100644
index 0000000..c0e8865
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-and-explicit-after.toml
@@ -0,0 +1,5 @@
+[a.b.c]
+answer = 42
+
+[a]
+better = 43
diff --git a/internal/toml-test/tests/valid/implicit-and-explicit-before.json b/internal/toml-test/tests/valid/implicit-and-explicit-before.json
new file mode 100644
index 0000000..663a34e
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-and-explicit-before.json
@@ -0,0 +1,16 @@
+{
+ "a": {
+ "b": {
+ "c": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ },
+ "better": {
+ "type": "integer",
+ "value": "43"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/implicit-and-explicit-before.toml b/internal/toml-test/tests/valid/implicit-and-explicit-before.toml
new file mode 100644
index 0000000..eee68ff
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-and-explicit-before.toml
@@ -0,0 +1,5 @@
+[a]
+better = 43
+
+[a.b.c]
+answer = 42
diff --git a/internal/toml-test/tests/valid/implicit-groups.json b/internal/toml-test/tests/valid/implicit-groups.json
new file mode 100644
index 0000000..347c693
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-groups.json
@@ -0,0 +1,12 @@
+{
+ "a": {
+ "b": {
+ "c": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/implicit-groups.toml b/internal/toml-test/tests/valid/implicit-groups.toml
new file mode 100644
index 0000000..b6333e4
--- /dev/null
+++ b/internal/toml-test/tests/valid/implicit-groups.toml
@@ -0,0 +1,2 @@
+[a.b.c]
+answer = 42
diff --git a/internal/toml-test/tests/valid/inline-table/array.json b/internal/toml-test/tests/valid/inline-table/array.json
new file mode 100644
index 0000000..fe78bf6
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/array.json
@@ -0,0 +1,34 @@
+{
+ "people": [
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Bruce"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Springsteen"
+ }
+ },
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Eric"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Clapton"
+ }
+ },
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Bob"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Seger"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/inline-table/array.toml b/internal/toml-test/tests/valid/inline-table/array.toml
new file mode 100644
index 0000000..3fa60d6
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/array.toml
@@ -0,0 +1,3 @@
+people = [{first_name = "Bruce", last_name = "Springsteen"},
+ {first_name = "Eric", last_name = "Clapton"},
+ {first_name = "Bob", last_name = "Seger"}]
diff --git a/internal/toml-test/tests/valid/inline-table/bool.json b/internal/toml-test/tests/valid/inline-table/bool.json
new file mode 100644
index 0000000..717952f
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/bool.json
@@ -0,0 +1,12 @@
+{
+ "a": {
+ "a": {
+ "type": "bool",
+ "value": "true"
+ },
+ "b": {
+ "type": "bool",
+ "value": "false"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/bool.toml b/internal/toml-test/tests/valid/inline-table/bool.toml
new file mode 100644
index 0000000..d228fc2
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/bool.toml
@@ -0,0 +1 @@
+a = {a = true, b = false}
diff --git a/internal/toml-test/tests/valid/inline-table/empty.json b/internal/toml-test/tests/valid/inline-table/empty.json
new file mode 100644
index 0000000..28beacf
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/empty.json
@@ -0,0 +1,30 @@
+{
+ "empty1": {},
+ "empty2": {},
+ "empty_in_array": [
+ {
+ "not_empty": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ {}
+ ],
+ "empty_in_array2": [
+ {},
+ {
+ "not_empty": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ ],
+ "many_empty": [
+ {},
+ {},
+ {}
+ ],
+ "nested_empty": {
+ "empty": {}
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/empty.toml b/internal/toml-test/tests/valid/inline-table/empty.toml
new file mode 100644
index 0000000..910d458
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/empty.toml
@@ -0,0 +1,6 @@
+empty1 = {}
+empty2 = { }
+empty_in_array = [ { not_empty = 1 }, {} ]
+empty_in_array2 = [{},{not_empty=1}]
+many_empty = [{},{},{}]
+nested_empty = {"empty"={}}
diff --git a/internal/toml-test/tests/valid/inline-table/end-in-bool.json b/internal/toml-test/tests/valid/inline-table/end-in-bool.json
new file mode 100644
index 0000000..90e700a
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/end-in-bool.json
@@ -0,0 +1,16 @@
+{
+ "black": {
+ "allow_prereleases": {
+ "type": "bool",
+ "value": "true"
+ },
+ "python": {
+ "type": "string",
+ "value": "\u003e3.6"
+ },
+ "version": {
+ "type": "string",
+ "value": "\u003e=18.9b0"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/end-in-bool.toml b/internal/toml-test/tests/valid/inline-table/end-in-bool.toml
new file mode 100644
index 0000000..94e5651
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/end-in-bool.toml
@@ -0,0 +1 @@
+black = { python=">3.6", version=">=18.9b0", allow_prereleases=true }
diff --git a/internal/toml-test/tests/valid/inline-table/inline-table.json b/internal/toml-test/tests/valid/inline-table/inline-table.json
new file mode 100644
index 0000000..2468f62
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/inline-table.json
@@ -0,0 +1,48 @@
+{
+ "name": {
+ "first": {
+ "type": "string",
+ "value": "Tom"
+ },
+ "last": {
+ "type": "string",
+ "value": "Preston-Werner"
+ }
+ },
+ "point": {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ },
+ "y": {
+ "type": "integer",
+ "value": "2"
+ }
+ },
+ "simple": {
+ "a": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ "str-key": {
+ "a": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ "table-array": [
+ {
+ "a": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ {
+ "b": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/inline-table/inline-table.toml b/internal/toml-test/tests/valid/inline-table/inline-table.toml
new file mode 100644
index 0000000..257047e
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/inline-table.toml
@@ -0,0 +1,5 @@
+name = { first = "Tom", last = "Preston-Werner" }
+point = { x = 1, y = 2 }
+simple = { a = 1 }
+str-key = { "a" = 1 }
+table-array = [{ "a" = 1 }, { "b" = 2 }]
diff --git a/internal/toml-test/tests/valid/inline-table/key-dotted.json b/internal/toml-test/tests/valid/inline-table/key-dotted.json
new file mode 100644
index 0000000..3886803
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/key-dotted.json
@@ -0,0 +1,140 @@
+{
+ "a": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "arr": [
+ {
+ "T": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "t": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ }
+ },
+ {
+ "T": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ },
+ "t": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ }
+ }
+ ],
+ "b": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "c": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "d": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "e": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "inline": {
+ "a": {
+ "b": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ },
+ "many": {
+ "dots": {
+ "here": {
+ "dot": {
+ "dot": {
+ "dot": {
+ "a": {
+ "b": {
+ "c": {
+ "type": "integer",
+ "value": "1"
+ },
+ "d": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tbl": {
+ "a": {
+ "b": {
+ "c": {
+ "d": {
+ "e": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ }
+ }
+ },
+ "x": {
+ "a": {
+ "b": {
+ "c": {
+ "d": {
+ "e": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/key-dotted.toml b/internal/toml-test/tests/valid/inline-table/key-dotted.toml
new file mode 100644
index 0000000..e812943
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/key-dotted.toml
@@ -0,0 +1,23 @@
+inline = {a.b = 42}
+
+many.dots.here.dot.dot.dot = {a.b.c = 1, a.b.d = 2}
+
+a = { a.b = 1 }
+b = { "a"."b" = 1 }
+c = { a . b = 1 }
+d = { 'a' . "b" = 1 }
+e = {a.b=1}
+
+[tbl]
+a.b.c = {d.e=1}
+
+[tbl.x]
+a.b.c = {d.e=1}
+
+[[arr]]
+t = {a.b=1}
+T = {a.b=1}
+
+[[arr]]
+t = {a.b=2}
+T = {a.b=2}
diff --git a/internal/toml-test/tests/valid/inline-table/multiline.json b/internal/toml-test/tests/valid/inline-table/multiline.json
new file mode 100644
index 0000000..f946d81
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/multiline.json
@@ -0,0 +1,20 @@
+{
+ "tbl_multiline": {
+ "a": {
+ "type": "integer",
+ "value": "1"
+ },
+ "b": {
+ "type": "string",
+ "value": "multiline\n"
+ },
+ "c": {
+ "type": "string",
+ "value": "and yet\nanother line"
+ },
+ "d": {
+ "type": "integer",
+ "value": "4"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/multiline.toml b/internal/toml-test/tests/valid/inline-table/multiline.toml
new file mode 100644
index 0000000..a653b18
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/multiline.toml
@@ -0,0 +1,4 @@
+tbl_multiline = { a = 1, b = """
+multiline
+""", c = """and yet
+another line""", d = 4 }
diff --git a/internal/toml-test/tests/valid/inline-table/nest.json b/internal/toml-test/tests/valid/inline-table/nest.json
new file mode 100644
index 0000000..70aebb3
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/nest.json
@@ -0,0 +1,64 @@
+{
+ "arr_arr_tbl_empty": [
+ [
+ {}
+ ]
+ ],
+ "arr_arr_tbl_val": [
+ [
+ {
+ "one": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ ]
+ ],
+ "arr_arr_tbls": [
+ [
+ {
+ "one": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ {
+ "two": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ ]
+ ],
+ "arr_tbl_tbl": [
+ {
+ "tbl": {
+ "one": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ }
+ ],
+ "tbl_arr_tbl": {
+ "arr_tbl": [
+ {
+ "one": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ ]
+ },
+ "tbl_tbl_empty": {
+ "tbl_0": {}
+ },
+ "tbl_tbl_val": {
+ "tbl_1": {
+ "one": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/nest.toml b/internal/toml-test/tests/valid/inline-table/nest.toml
new file mode 100644
index 0000000..2624c1d
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/nest.toml
@@ -0,0 +1,10 @@
+tbl_tbl_empty = { tbl_0 = {} }
+tbl_tbl_val = { tbl_1 = { one = 1 } }
+tbl_arr_tbl = { arr_tbl = [ { one = 1 } ] }
+arr_tbl_tbl = [ { tbl = { one = 1 } } ]
+
+# Array-of-array-of-table is interesting because it can only
+# be represented in inline form.
+arr_arr_tbl_empty = [ [ {} ] ]
+arr_arr_tbl_val = [ [ { one = 1 } ] ]
+arr_arr_tbls = [ [ { one = 1 }, { two = 2 } ] ]
diff --git a/internal/toml-test/tests/valid/inline-table/newline.json b/internal/toml-test/tests/valid/inline-table/newline.json
new file mode 100644
index 0000000..c19e588
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/newline.json
@@ -0,0 +1,50 @@
+{
+ "tbl-1": {
+ "1": {
+ "type": "integer",
+ "value": "2"
+ },
+ "arr": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ],
+ "hello": {
+ "type": "string",
+ "value": "world"
+ },
+ "tbl": {
+ "k": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ },
+ "tbl-2": {
+ "k": {
+ "type": "string",
+ "value": "\tHello\n\t"
+ }
+ },
+ "trailing-comma-1": {
+ "c": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ "trailing-comma-2": {
+ "c": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/inline-table/newline.toml b/internal/toml-test/tests/valid/inline-table/newline.toml
new file mode 100644
index 0000000..914afe9
--- /dev/null
+++ b/internal/toml-test/tests/valid/inline-table/newline.toml
@@ -0,0 +1,24 @@
+# TOML 1.1 supports newlines in inline tables and trailing commas.
+
+trailing-comma-1 = {
+ c = 1,
+}
+trailing-comma-2 = { c = 1, }
+
+tbl-1 = {
+ hello = "world",
+ 1 = 2,
+ arr = [1,
+ 2,
+ 3,
+ ],
+ tbl = {
+ k = 1,
+ }
+}
+
+tbl-2 = {
+ k = """
+ Hello
+ """
+}
diff --git a/internal/toml-test/tests/valid/integer/integer.json b/internal/toml-test/tests/valid/integer/integer.json
new file mode 100644
index 0000000..eee9758
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/integer.json
@@ -0,0 +1,18 @@
+{
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ },
+ "neganswer": {
+ "type": "integer",
+ "value": "-42"
+ },
+ "posanswer": {
+ "type": "integer",
+ "value": "42"
+ },
+ "zero": {
+ "type": "integer",
+ "value": "0"
+ }
+}
diff --git a/internal/toml-test/tests/valid/integer/integer.toml b/internal/toml-test/tests/valid/integer/integer.toml
new file mode 100644
index 0000000..b62de30
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/integer.toml
@@ -0,0 +1,4 @@
+answer = 42
+posanswer = +42
+neganswer = -42
+zero = 0
diff --git a/internal/toml-test/tests/valid/integer/literals.json b/internal/toml-test/tests/valid/integer/literals.json
new file mode 100644
index 0000000..1a8555b
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/literals.json
@@ -0,0 +1,38 @@
+{
+ "bin1": {
+ "type": "integer",
+ "value": "214"
+ },
+ "bin2": {
+ "type": "integer",
+ "value": "5"
+ },
+ "hex1": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "hex2": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "hex3": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "hex4": {
+ "type": "integer",
+ "value": "2439"
+ },
+ "oct1": {
+ "type": "integer",
+ "value": "342391"
+ },
+ "oct2": {
+ "type": "integer",
+ "value": "493"
+ },
+ "oct3": {
+ "type": "integer",
+ "value": "501"
+ }
+}
diff --git a/internal/toml-test/tests/valid/integer/literals.toml b/internal/toml-test/tests/valid/integer/literals.toml
new file mode 100644
index 0000000..a078590
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/literals.toml
@@ -0,0 +1,11 @@
+bin1 = 0b11010110
+bin2 = 0b1_0_1
+
+oct1 = 0o01234567
+oct2 = 0o755
+oct3 = 0o7_6_5
+
+hex1 = 0xDEADBEEF
+hex2 = 0xdeadbeef
+hex3 = 0xdead_beef
+hex4 = 0x00987
diff --git a/internal/toml-test/tests/valid/integer/long.json b/internal/toml-test/tests/valid/integer/long.json
new file mode 100644
index 0000000..6f4f0a7
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/long.json
@@ -0,0 +1,10 @@
+{
+ "int64-max": {
+ "type": "integer",
+ "value": "9223372036854775807"
+ },
+ "int64-max-neg": {
+ "type": "integer",
+ "value": "-9223372036854775808"
+ }
+}
diff --git a/internal/toml-test/tests/valid/integer/long.toml b/internal/toml-test/tests/valid/integer/long.toml
new file mode 100644
index 0000000..5db5ed7
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/long.toml
@@ -0,0 +1,2 @@
+int64-max = 9223372036854775807
+int64-max-neg = -9223372036854775808
diff --git a/internal/toml-test/tests/valid/integer/underscore.json b/internal/toml-test/tests/valid/integer/underscore.json
new file mode 100644
index 0000000..10b9b59
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/underscore.json
@@ -0,0 +1,10 @@
+{
+ "kilo": {
+ "type": "integer",
+ "value": "1000"
+ },
+ "x": {
+ "type": "integer",
+ "value": "1111"
+ }
+}
diff --git a/internal/toml-test/tests/valid/integer/underscore.toml b/internal/toml-test/tests/valid/integer/underscore.toml
new file mode 100644
index 0000000..7c2da5f
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/underscore.toml
@@ -0,0 +1,2 @@
+kilo = 1_000
+x = 1_1_1_1
diff --git a/internal/toml-test/tests/valid/integer/zero.json b/internal/toml-test/tests/valid/integer/zero.json
new file mode 100644
index 0000000..9d5c707
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/zero.json
@@ -0,0 +1,50 @@
+{
+ "a2": {
+ "type": "integer",
+ "value": "0"
+ },
+ "a3": {
+ "type": "integer",
+ "value": "0"
+ },
+ "b1": {
+ "type": "integer",
+ "value": "0"
+ },
+ "b2": {
+ "type": "integer",
+ "value": "0"
+ },
+ "b3": {
+ "type": "integer",
+ "value": "0"
+ },
+ "d1": {
+ "type": "integer",
+ "value": "0"
+ },
+ "d2": {
+ "type": "integer",
+ "value": "0"
+ },
+ "d3": {
+ "type": "integer",
+ "value": "0"
+ },
+ "h1": {
+ "type": "integer",
+ "value": "0"
+ },
+ "h2": {
+ "type": "integer",
+ "value": "0"
+ },
+ "h3": {
+ "type": "integer",
+ "value": "0"
+ },
+ "o1": {
+ "type": "integer",
+ "value": "0"
+ }
+}
diff --git a/internal/toml-test/tests/valid/integer/zero.toml b/internal/toml-test/tests/valid/integer/zero.toml
new file mode 100644
index 0000000..4687c28
--- /dev/null
+++ b/internal/toml-test/tests/valid/integer/zero.toml
@@ -0,0 +1,15 @@
+d1 = 0
+d2 = +0
+d3 = -0
+
+h1 = 0x0
+h2 = 0x00
+h3 = 0x00000
+
+o1 = 0o0
+a2 = 0o00
+a3 = 0o00000
+
+b1 = 0b0
+b2 = 0b00
+b3 = 0b00000
diff --git a/internal/toml-test/tests/valid/key/alphanum.json b/internal/toml-test/tests/valid/key/alphanum.json
new file mode 100644
index 0000000..a54e1d3
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/alphanum.json
@@ -0,0 +1,46 @@
+{
+ "000111": {
+ "type": "string",
+ "value": "leading"
+ },
+ "10e3": {
+ "type": "string",
+ "value": "false float"
+ },
+ "123": {
+ "type": "string",
+ "value": "num"
+ },
+ "2018_10": {
+ "001": {
+ "type": "integer",
+ "value": "1"
+ }
+ },
+ "34-11": {
+ "type": "integer",
+ "value": "23"
+ },
+ "a-a-a": {
+ "_": {
+ "type": "bool",
+ "value": "false"
+ }
+ },
+ "alpha": {
+ "type": "string",
+ "value": "a"
+ },
+ "one1two2": {
+ "type": "string",
+ "value": "mixed"
+ },
+ "under_score": {
+ "type": "string",
+ "value": "___"
+ },
+ "with-dash": {
+ "type": "string",
+ "value": "dashed"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/alphanum.toml b/internal/toml-test/tests/valid/key/alphanum.toml
new file mode 100644
index 0000000..0632e9c
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/alphanum.toml
@@ -0,0 +1,14 @@
+alpha = "a"
+123 = "num"
+000111 = "leading"
+10e3 = "false float"
+one1two2 = "mixed"
+with-dash = "dashed"
+under_score = "___"
+34-11 = 23
+
+[2018_10]
+001 = 1
+
+[a-a-a]
+_ = false
diff --git a/internal/toml-test/tests/valid/key/case-sensitive.json b/internal/toml-test/tests/valid/key/case-sensitive.json
new file mode 100644
index 0000000..1f205d2
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/case-sensitive.json
@@ -0,0 +1,38 @@
+{
+ "Section": {
+ "M": {
+ "type": "string",
+ "value": "latin letter M"
+ },
+ "name": {
+ "type": "string",
+ "value": "different section!!"
+ },
+ "Μ": {
+ "type": "string",
+ "value": "greek capital letter MU"
+ },
+ "μ": {
+ "type": "string",
+ "value": "greek small letter mu"
+ }
+ },
+ "sectioN": {
+ "type": "string",
+ "value": "NN"
+ },
+ "section": {
+ "NAME": {
+ "type": "string",
+ "value": "upper"
+ },
+ "Name": {
+ "type": "string",
+ "value": "capitalized"
+ },
+ "name": {
+ "type": "string",
+ "value": "lower"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/case-sensitive.toml b/internal/toml-test/tests/valid/key/case-sensitive.toml
new file mode 100644
index 0000000..e02b3a5
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/case-sensitive.toml
@@ -0,0 +1,13 @@
+sectioN = "NN"
+
+[section]
+name = "lower"
+NAME = "upper"
+Name = "capitalized"
+
+[Section]
+name = "different section!!"
+"μ" = "greek small letter mu"
+"Μ" = "greek capital letter MU"
+M = "latin letter M"
+
diff --git a/internal/toml-test/tests/valid/key/dotted-empty.json b/internal/toml-test/tests/valid/key/dotted-empty.json
new file mode 100644
index 0000000..4ca4b50
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/dotted-empty.json
@@ -0,0 +1,22 @@
+{
+ "": {
+ "x": {
+ "type": "string",
+ "value": "empty.x"
+ }
+ },
+ "x": {
+ "": {
+ "type": "string",
+ "value": "x.empty"
+ }
+ },
+ "a": {
+ "": {
+ "": {
+ "type": "string",
+ "value": "empty.empty"
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/dotted-empty.toml b/internal/toml-test/tests/valid/key/dotted-empty.toml
new file mode 100644
index 0000000..45ef4c7
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/dotted-empty.toml
@@ -0,0 +1,4 @@
+''.x = "empty.x"
+x."" = "x.empty"
+[a]
+"".'' = "empty.empty"
diff --git a/internal/toml-test/tests/valid/key/dotted.json b/internal/toml-test/tests/valid/key/dotted.json
new file mode 100644
index 0000000..a527a72
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/dotted.json
@@ -0,0 +1,132 @@
+{
+ "a": {
+ "few": {
+ "dots": {
+ "polka": {
+ "dance-with": {
+ "type": "string",
+ "value": "Dot"
+ },
+ "dot": {
+ "type": "string",
+ "value": "again?"
+ }
+ }
+ }
+ }
+ },
+ "arr": [
+ {
+ "a": {
+ "b": {
+ "c": {
+ "type": "integer",
+ "value": "1"
+ },
+ "d": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ }
+ },
+ {
+ "a": {
+ "b": {
+ "c": {
+ "type": "integer",
+ "value": "3"
+ },
+ "d": {
+ "type": "integer",
+ "value": "4"
+ }
+ }
+ }
+ }
+ ],
+ "count": {
+ "a": {
+ "type": "integer",
+ "value": "1"
+ },
+ "b": {
+ "type": "integer",
+ "value": "2"
+ },
+ "c": {
+ "type": "integer",
+ "value": "3"
+ },
+ "d": {
+ "type": "integer",
+ "value": "4"
+ },
+ "e": {
+ "type": "integer",
+ "value": "5"
+ },
+ "f": {
+ "type": "integer",
+ "value": "6"
+ },
+ "g": {
+ "type": "integer",
+ "value": "7"
+ },
+ "h": {
+ "type": "integer",
+ "value": "8"
+ },
+ "i": {
+ "type": "integer",
+ "value": "9"
+ },
+ "j": {
+ "type": "integer",
+ "value": "10"
+ },
+ "k": {
+ "type": "integer",
+ "value": "11"
+ },
+ "l": {
+ "type": "integer",
+ "value": "12"
+ }
+ },
+ "many": {
+ "dots": {
+ "here": {
+ "dot": {
+ "dot": {
+ "dot": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ }
+ }
+ }
+ },
+ "name": {
+ "first": {
+ "type": "string",
+ "value": "Arthur"
+ },
+ "last": {
+ "type": "string",
+ "value": "Dent"
+ }
+ },
+ "tbl": {
+ "a": {
+ "b": {
+ "c": {
+ "type": "float",
+ "value": "42.666"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/dotted.toml b/internal/toml-test/tests/valid/key/dotted.toml
new file mode 100644
index 0000000..52d62d7
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/dotted.toml
@@ -0,0 +1,35 @@
+# Note: this file contains literal tab characters.
+
+name.first = "Arthur"
+"name".'last' = "Dent"
+
+many.dots.here.dot.dot.dot = 42
+
+# Space are ignored, and key parts can be quoted.
+count.a = 1
+count . b = 2
+"count"."c" = 3
+"count" . "d" = 4
+'count'.'e' = 5
+'count' . 'f' = 6
+"count".'g' = 7
+"count" . 'h' = 8
+count.'i' = 9
+count . 'j' = 10
+"count".k = 11
+"count" . l = 12
+
+[tbl]
+a.b.c = 42.666
+
+[a.few.dots]
+polka.dot = "again?"
+polka.dance-with = "Dot"
+
+[[arr]]
+a.b.c=1
+a.b.d=2
+
+[[arr]]
+a.b.c=3
+a.b.d=4
diff --git a/internal/toml-test/tests/valid/key/empty.json b/internal/toml-test/tests/valid/key/empty.json
new file mode 100644
index 0000000..44948a9
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/empty.json
@@ -0,0 +1,6 @@
+{
+ "": {
+ "type": "string",
+ "value": "blank"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/empty.toml b/internal/toml-test/tests/valid/key/empty.toml
new file mode 100644
index 0000000..0a1640d
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/empty.toml
@@ -0,0 +1 @@
+"" = "blank"
diff --git a/internal/toml-test/tests/valid/key/equals-nospace.json b/internal/toml-test/tests/valid/key/equals-nospace.json
new file mode 100644
index 0000000..9394e56
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/equals-nospace.json
@@ -0,0 +1,6 @@
+{
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/equals-nospace.toml b/internal/toml-test/tests/valid/key/equals-nospace.toml
new file mode 100644
index 0000000..560901c
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/equals-nospace.toml
@@ -0,0 +1 @@
+answer=42
diff --git a/internal/toml-test/tests/valid/key/escapes.json b/internal/toml-test/tests/valid/key/escapes.json
new file mode 100644
index 0000000..e62b3a1
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/escapes.json
@@ -0,0 +1,24 @@
+{
+ "\n": {
+ "type": "string",
+ "value": "newline"
+ },
+ "\"": {
+ "type": "string",
+ "value": "just a quote"
+ },
+ "\"quoted\"": {
+ "quote": {
+ "type": "bool",
+ "value": "true"
+ }
+ },
+ "a.b": {
+ "À": {}
+ },
+ "backsp\u0008\u0008": {},
+ "À": {
+ "type": "string",
+ "value": "latin capital letter A with grave"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/escapes.toml b/internal/toml-test/tests/valid/key/escapes.toml
new file mode 100644
index 0000000..aac3c0a
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/escapes.toml
@@ -0,0 +1,10 @@
+"\n" = "newline"
+"\u00c0" = "latin capital letter A with grave"
+"\"" = "just a quote"
+
+["backsp\b\b"]
+
+["\"quoted\""]
+quote = true
+
+["a.b"."\u00c0"]
diff --git a/internal/toml-test/tests/valid/key/numeric-dotted.json b/internal/toml-test/tests/valid/key/numeric-dotted.json
new file mode 100644
index 0000000..43dd4d1
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/numeric-dotted.json
@@ -0,0 +1,8 @@
+{
+ "1": {
+ "2": {
+ "type": "integer",
+ "value": "3"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/numeric-dotted.toml b/internal/toml-test/tests/valid/key/numeric-dotted.toml
new file mode 100644
index 0000000..538204c
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/numeric-dotted.toml
@@ -0,0 +1 @@
+1.2 = 3
diff --git a/internal/toml-test/tests/valid/key/numeric.json b/internal/toml-test/tests/valid/key/numeric.json
new file mode 100644
index 0000000..eddb88b
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/numeric.json
@@ -0,0 +1,6 @@
+{
+ "1": {
+ "type": "integer",
+ "value": "1"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/numeric.toml b/internal/toml-test/tests/valid/key/numeric.toml
new file mode 100644
index 0000000..532356f
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/numeric.toml
@@ -0,0 +1 @@
+1 = 1
diff --git a/internal/toml-test/tests/valid/key/quoted-dots.json b/internal/toml-test/tests/valid/key/quoted-dots.json
new file mode 100644
index 0000000..1582c5b
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/quoted-dots.json
@@ -0,0 +1,32 @@
+{
+ "plain": {
+ "type": "integer",
+ "value": "1"
+ },
+ "plain_table": {
+ "plain": {
+ "type": "integer",
+ "value": "3"
+ },
+ "with.dot": {
+ "type": "integer",
+ "value": "4"
+ }
+ },
+ "table": {
+ "withdot": {
+ "key.with.dots": {
+ "type": "integer",
+ "value": "6"
+ },
+ "plain": {
+ "type": "integer",
+ "value": "5"
+ }
+ }
+ },
+ "with.dot": {
+ "type": "integer",
+ "value": "2"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/quoted-dots.toml b/internal/toml-test/tests/valid/key/quoted-dots.toml
new file mode 100644
index 0000000..65fcddf
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/quoted-dots.toml
@@ -0,0 +1,10 @@
+plain = 1
+"with.dot" = 2
+
+[plain_table]
+plain = 3
+"with.dot" = 4
+
+[table.withdot]
+plain = 5
+"key.with.dots" = 6
diff --git a/internal/toml-test/tests/valid/key/quoted-unicode.json b/internal/toml-test/tests/valid/key/quoted-unicode.json
new file mode 100644
index 0000000..a51cc01
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/quoted-unicode.json
@@ -0,0 +1,22 @@
+{
+ "\u0000": {
+ "type": "string",
+ "value": "null"
+ },
+ "\\u0000": {
+ "type": "string",
+ "value": "different key"
+ },
+ "\b \f A \u007f \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff": {
+ "type": "string",
+ "value": "escaped key"
+ },
+ "~ \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff": {
+ "type": "string",
+ "value": "basic key"
+ },
+ "l ~ \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff": {
+ "type": "string",
+ "value": "literal key"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/quoted-unicode.toml b/internal/toml-test/tests/valid/key/quoted-unicode.toml
new file mode 100644
index 0000000..bc31b72
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/quoted-unicode.toml
@@ -0,0 +1,7 @@
+
+"\u0000" = "null"
+'\u0000' = "different key"
+"\u0008 \u000c \U00000041 \u007f \u0080 \u00ff \ud7ff \ue000 \uffff \U00010000 \U0010ffff" = "escaped key"
+
+"~ € ÿ ퟿  ￿ 𐀀 􏿿" = "basic key"
+'l ~ € ÿ ퟿  ￿ 𐀀 􏿿' = "literal key"
diff --git a/internal/toml-test/tests/valid/key/space.json b/internal/toml-test/tests/valid/key/space.json
new file mode 100644
index 0000000..670d16a
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/space.json
@@ -0,0 +1,16 @@
+{
+ " c d ": {
+ "type": "integer",
+ "value": "2"
+ },
+ " tbl ": {
+ "\ttab\ttab\t": {
+ "type": "string",
+ "value": "tab"
+ }
+ },
+ "a b": {
+ "type": "integer",
+ "value": "1"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/space.toml b/internal/toml-test/tests/valid/key/space.toml
new file mode 100644
index 0000000..4234f88
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/space.toml
@@ -0,0 +1,6 @@
+# Keep whitespace inside quotes keys at all positions.
+"a b" = 1
+" c d " = 2
+
+[ " tbl " ]
+"\ttab\ttab\t" = "tab"
diff --git a/internal/toml-test/tests/valid/key/special-chars.json b/internal/toml-test/tests/valid/key/special-chars.json
new file mode 100644
index 0000000..fa64c7d
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/special-chars.json
@@ -0,0 +1,6 @@
+{
+ "=~!@$^\u0026*()_+-`1234567890[]|/?\u003e\u003c.,;:'=": {
+ "type": "integer",
+ "value": "1"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/special-chars.toml b/internal/toml-test/tests/valid/key/special-chars.toml
new file mode 100644
index 0000000..0ba7168
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/special-chars.toml
@@ -0,0 +1 @@
+"=~!@$^&*()_+-`1234567890[]|/?><.,;:'=" = 1
diff --git a/internal/toml-test/tests/valid/key/special-word.json b/internal/toml-test/tests/valid/key/special-word.json
new file mode 100644
index 0000000..59ce693
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/special-word.json
@@ -0,0 +1,18 @@
+{
+ "false": {
+ "type": "bool",
+ "value": "false"
+ },
+ "inf": {
+ "type": "integer",
+ "value": "100000000"
+ },
+ "nan": {
+ "type": "string",
+ "value": "ceci n'est pas un nombre"
+ },
+ "true": {
+ "type": "integer",
+ "value": "1"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/special-word.toml b/internal/toml-test/tests/valid/key/special-word.toml
new file mode 100644
index 0000000..0b836f1
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/special-word.toml
@@ -0,0 +1,5 @@
+false = false
+true = 1
+inf = 100000000
+nan = "ceci n'est pas un nombre"
+
diff --git a/internal/toml-test/tests/valid/key/unicode.json b/internal/toml-test/tests/valid/key/unicode.json
new file mode 100644
index 0000000..62ae54e
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/unicode.json
@@ -0,0 +1,18 @@
+{
+ "a‍b": {
+ "type": "string",
+ "value": "zwj"
+ },
+ "ÅÅ": {
+ "type": "string",
+ "value": "U+00C5 U+0041 U+030A"
+ },
+ "€": {
+ "type": "string",
+ "value": "Euro"
+ },
+ "😂": {
+ "type": "string",
+ "value": "rofl"
+ }
+}
diff --git a/internal/toml-test/tests/valid/key/unicode.toml b/internal/toml-test/tests/valid/key/unicode.toml
new file mode 100644
index 0000000..0bc75b9
--- /dev/null
+++ b/internal/toml-test/tests/valid/key/unicode.toml
@@ -0,0 +1,6 @@
+# TOML 1.1 supports Unicode for bare keys.
+
+€ = 'Euro'
+😂 = "rofl"
+a‍b = "zwj"
+ÅÅ = "U+00C5 U+0041 U+030A"
diff --git a/internal/toml-test/tests/valid/newline-crlf.json b/internal/toml-test/tests/valid/newline-crlf.json
new file mode 100644
index 0000000..acf617e
--- /dev/null
+++ b/internal/toml-test/tests/valid/newline-crlf.json
@@ -0,0 +1,10 @@
+{
+ "newline": {
+ "type": "string",
+ "value": "crlf"
+ },
+ "os": {
+ "type": "string",
+ "value": "DOS"
+ }
+}
diff --git a/internal/toml-test/tests/valid/newline-crlf.toml b/internal/toml-test/tests/valid/newline-crlf.toml
new file mode 100644
index 0000000..0030024
--- /dev/null
+++ b/internal/toml-test/tests/valid/newline-crlf.toml
@@ -0,0 +1,2 @@
+os = "DOS"
+newline = "crlf"
diff --git a/internal/toml-test/tests/valid/newline-lf.json b/internal/toml-test/tests/valid/newline-lf.json
new file mode 100644
index 0000000..66cf871
--- /dev/null
+++ b/internal/toml-test/tests/valid/newline-lf.json
@@ -0,0 +1,10 @@
+{
+ "newline": {
+ "type": "string",
+ "value": "lf"
+ },
+ "os": {
+ "type": "string",
+ "value": "unix"
+ }
+}
diff --git a/internal/toml-test/tests/valid/newline-lf.toml b/internal/toml-test/tests/valid/newline-lf.toml
new file mode 100644
index 0000000..0f3377c
--- /dev/null
+++ b/internal/toml-test/tests/valid/newline-lf.toml
@@ -0,0 +1,2 @@
+os = "unix"
+newline = "lf"
diff --git a/internal/toml-test/tests/valid/spec-example-1-compact.json b/internal/toml-test/tests/valid/spec-example-1-compact.json
new file mode 100644
index 0000000..9f4078a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec-example-1-compact.json
@@ -0,0 +1,100 @@
+{
+ "clients": {
+ "data": [
+ [
+ {
+ "type": "string",
+ "value": "gamma"
+ },
+ {
+ "type": "string",
+ "value": "delta"
+ }
+ ],
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ]
+ ],
+ "hosts": [
+ {
+ "type": "string",
+ "value": "alpha"
+ },
+ {
+ "type": "string",
+ "value": "omega"
+ }
+ ]
+ },
+ "database": {
+ "connection_max": {
+ "type": "integer",
+ "value": "5000"
+ },
+ "enabled": {
+ "type": "bool",
+ "value": "true"
+ },
+ "ports": [
+ {
+ "type": "integer",
+ "value": "8001"
+ },
+ {
+ "type": "integer",
+ "value": "8001"
+ },
+ {
+ "type": "integer",
+ "value": "8002"
+ }
+ ],
+ "server": {
+ "type": "string",
+ "value": "192.168.1.1"
+ }
+ },
+ "owner": {
+ "dob": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00-08:00"
+ },
+ "name": {
+ "type": "string",
+ "value": "Lance Uppercut"
+ }
+ },
+ "servers": {
+ "alpha": {
+ "dc": {
+ "type": "string",
+ "value": "eqdc10"
+ },
+ "ip": {
+ "type": "string",
+ "value": "10.0.0.1"
+ }
+ },
+ "beta": {
+ "dc": {
+ "type": "string",
+ "value": "eqdc10"
+ },
+ "ip": {
+ "type": "string",
+ "value": "10.0.0.2"
+ }
+ }
+ },
+ "title": {
+ "type": "string",
+ "value": "TOML Example"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec-example-1-compact.toml b/internal/toml-test/tests/valid/spec-example-1-compact.toml
new file mode 100644
index 0000000..15e2b84
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec-example-1-compact.toml
@@ -0,0 +1,23 @@
+#Useless spaces eliminated.
+title="TOML Example"
+[owner]
+name="Lance Uppercut"
+dob=1979-05-27T07:32:00-08:00#First class dates
+[database]
+server="192.168.1.1"
+ports=[8001,8001,8002]
+connection_max=5000
+enabled=true
+[servers]
+[servers.alpha]
+ip="10.0.0.1"
+dc="eqdc10"
+[servers.beta]
+ip="10.0.0.2"
+dc="eqdc10"
+[clients]
+data=[["gamma","delta"],[1,2]]
+hosts=[
+"alpha",
+"omega"
+]
diff --git a/internal/toml-test/tests/valid/spec-example-1.json b/internal/toml-test/tests/valid/spec-example-1.json
new file mode 100644
index 0000000..9f4078a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec-example-1.json
@@ -0,0 +1,100 @@
+{
+ "clients": {
+ "data": [
+ [
+ {
+ "type": "string",
+ "value": "gamma"
+ },
+ {
+ "type": "string",
+ "value": "delta"
+ }
+ ],
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ]
+ ],
+ "hosts": [
+ {
+ "type": "string",
+ "value": "alpha"
+ },
+ {
+ "type": "string",
+ "value": "omega"
+ }
+ ]
+ },
+ "database": {
+ "connection_max": {
+ "type": "integer",
+ "value": "5000"
+ },
+ "enabled": {
+ "type": "bool",
+ "value": "true"
+ },
+ "ports": [
+ {
+ "type": "integer",
+ "value": "8001"
+ },
+ {
+ "type": "integer",
+ "value": "8001"
+ },
+ {
+ "type": "integer",
+ "value": "8002"
+ }
+ ],
+ "server": {
+ "type": "string",
+ "value": "192.168.1.1"
+ }
+ },
+ "owner": {
+ "dob": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00-08:00"
+ },
+ "name": {
+ "type": "string",
+ "value": "Lance Uppercut"
+ }
+ },
+ "servers": {
+ "alpha": {
+ "dc": {
+ "type": "string",
+ "value": "eqdc10"
+ },
+ "ip": {
+ "type": "string",
+ "value": "10.0.0.1"
+ }
+ },
+ "beta": {
+ "dc": {
+ "type": "string",
+ "value": "eqdc10"
+ },
+ "ip": {
+ "type": "string",
+ "value": "10.0.0.2"
+ }
+ }
+ },
+ "title": {
+ "type": "string",
+ "value": "TOML Example"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec-example-1.toml b/internal/toml-test/tests/valid/spec-example-1.toml
new file mode 100644
index 0000000..52fd1e8
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec-example-1.toml
@@ -0,0 +1,33 @@
+# This is a TOML document. Boom.
+
+title = "TOML Example"
+
+[owner]
+name = "Lance Uppercut"
+dob = 1979-05-27T07:32:00-08:00 # First class dates? Why not?
+
+[database]
+server = "192.168.1.1"
+ports = [ 8001, 8001, 8002 ]
+connection_max = 5000
+enabled = true
+
+[servers]
+
+ # You can indent as you please. Tabs or spaces. TOML don't care.
+ [servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+ [servers.beta]
+ ip = "10.0.0.2"
+ dc = "eqdc10"
+
+[clients]
+data = [ ["gamma", "delta"], [1, 2] ]
+
+# Line breaks are OK when inside arrays
+hosts = [
+ "alpha",
+ "omega"
+]
diff --git a/internal/toml-test/tests/valid/spec/array-0.json b/internal/toml-test/tests/valid/spec/array-0.json
new file mode 100644
index 0000000..f150e9d
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-0.json
@@ -0,0 +1,146 @@
+{
+ "colors": [
+ {
+ "type": "string",
+ "value": "red"
+ },
+ {
+ "type": "string",
+ "value": "yellow"
+ },
+ {
+ "type": "string",
+ "value": "green"
+ }
+ ],
+ "contributors": [
+ {
+ "type": "string",
+ "value": "Foo Bar \u003cfoo@example.com\u003e"
+ },
+ {
+ "email": {
+ "type": "string",
+ "value": "bazqux@example.com"
+ },
+ "name": {
+ "type": "string",
+ "value": "Baz Qux"
+ },
+ "url": {
+ "type": "string",
+ "value": "https://example.com/bazqux"
+ }
+ }
+ ],
+ "integers": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ],
+ "nested_arrays_of_ints": [
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ],
+ [
+ {
+ "type": "integer",
+ "value": "3"
+ },
+ {
+ "type": "integer",
+ "value": "4"
+ },
+ {
+ "type": "integer",
+ "value": "5"
+ }
+ ]
+ ],
+ "nested_mixed_array": [
+ [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ],
+ [
+ {
+ "type": "string",
+ "value": "a"
+ },
+ {
+ "type": "string",
+ "value": "b"
+ },
+ {
+ "type": "string",
+ "value": "c"
+ }
+ ]
+ ],
+ "numbers": [
+ {
+ "type": "float",
+ "value": "0.1"
+ },
+ {
+ "type": "float",
+ "value": "0.2"
+ },
+ {
+ "type": "float",
+ "value": "0.5"
+ },
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "5"
+ }
+ ],
+ "string_array": [
+ {
+ "type": "string",
+ "value": "all"
+ },
+ {
+ "type": "string",
+ "value": "strings"
+ },
+ {
+ "type": "string",
+ "value": "are the same"
+ },
+ {
+ "type": "string",
+ "value": "type"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/spec/array-0.toml b/internal/toml-test/tests/valid/spec/array-0.toml
new file mode 100644
index 0000000..467a8bc
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-0.toml
@@ -0,0 +1,12 @@
+integers = [ 1, 2, 3 ]
+colors = [ "red", "yellow", "green" ]
+nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]
+nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]
+string_array = [ "all", 'strings', """are the same""", '''type''' ]
+
+# Mixed-type arrays are allowed
+numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ]
+contributors = [
+ "Foo Bar <foo@example.com>",
+ { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
+]
diff --git a/internal/toml-test/tests/valid/spec/array-1.json b/internal/toml-test/tests/valid/spec/array-1.json
new file mode 100644
index 0000000..334c333
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-1.json
@@ -0,0 +1,26 @@
+{
+ "integers2": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ },
+ {
+ "type": "integer",
+ "value": "3"
+ }
+ ],
+ "integers3": [
+ {
+ "type": "integer",
+ "value": "1"
+ },
+ {
+ "type": "integer",
+ "value": "2"
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/spec/array-1.toml b/internal/toml-test/tests/valid/spec/array-1.toml
new file mode 100644
index 0000000..6acd9ca
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-1.toml
@@ -0,0 +1,8 @@
+integers2 = [
+ 1, 2, 3
+]
+
+integers3 = [
+ 1,
+ 2, # this is ok
+]
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-0.json b/internal/toml-test/tests/valid/spec/array-of-tables-0.json
new file mode 100644
index 0000000..2328757
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-0.json
@@ -0,0 +1,29 @@
+{
+ "products": [
+ {
+ "name": {
+ "type": "string",
+ "value": "Hammer"
+ },
+ "sku": {
+ "type": "integer",
+ "value": "738594937"
+ }
+ },
+ {},
+ {
+ "color": {
+ "type": "string",
+ "value": "gray"
+ },
+ "name": {
+ "type": "string",
+ "value": "Nail"
+ },
+ "sku": {
+ "type": "integer",
+ "value": "284758393"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-0.toml b/internal/toml-test/tests/valid/spec/array-of-tables-0.toml
new file mode 100644
index 0000000..544d142
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-0.toml
@@ -0,0 +1,11 @@
+[[products]]
+name = "Hammer"
+sku = 738594937
+
+[[products]] # empty table within the array
+
+[[products]]
+name = "Nail"
+sku = 284758393
+
+color = "gray"
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-1.json b/internal/toml-test/tests/valid/spec/array-of-tables-1.json
new file mode 100644
index 0000000..17d4918
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-1.json
@@ -0,0 +1,48 @@
+{
+ "fruits": [
+ {
+ "name": {
+ "type": "string",
+ "value": "apple"
+ },
+ "physical": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "shape": {
+ "type": "string",
+ "value": "round"
+ }
+ },
+ "varieties": [
+ {
+ "name": {
+ "type": "string",
+ "value": "red delicious"
+ }
+ },
+ {
+ "name": {
+ "type": "string",
+ "value": "granny smith"
+ }
+ }
+ ]
+ },
+ {
+ "name": {
+ "type": "string",
+ "value": "banana"
+ },
+ "varieties": [
+ {
+ "name": {
+ "type": "string",
+ "value": "plantain"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-1.toml b/internal/toml-test/tests/valid/spec/array-of-tables-1.toml
new file mode 100644
index 0000000..7a3971f
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-1.toml
@@ -0,0 +1,19 @@
+[[fruits]]
+name = "apple"
+
+[fruits.physical] # subtable
+color = "red"
+shape = "round"
+
+[[fruits.varieties]] # nested array of tables
+name = "red delicious"
+
+[[fruits.varieties]]
+name = "granny smith"
+
+
+[[fruits]]
+name = "banana"
+
+[[fruits.varieties]]
+name = "plantain"
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-2.json b/internal/toml-test/tests/valid/spec/array-of-tables-2.json
new file mode 100644
index 0000000..f2e3f36
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-2.json
@@ -0,0 +1,46 @@
+{
+ "points": [
+ {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ },
+ "y": {
+ "type": "integer",
+ "value": "2"
+ },
+ "z": {
+ "type": "integer",
+ "value": "3"
+ }
+ },
+ {
+ "x": {
+ "type": "integer",
+ "value": "7"
+ },
+ "y": {
+ "type": "integer",
+ "value": "8"
+ },
+ "z": {
+ "type": "integer",
+ "value": "9"
+ }
+ },
+ {
+ "x": {
+ "type": "integer",
+ "value": "2"
+ },
+ "y": {
+ "type": "integer",
+ "value": "4"
+ },
+ "z": {
+ "type": "integer",
+ "value": "8"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/spec/array-of-tables-2.toml b/internal/toml-test/tests/valid/spec/array-of-tables-2.toml
new file mode 100644
index 0000000..ba5584a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/array-of-tables-2.toml
@@ -0,0 +1,3 @@
+points = [ { x = 1, y = 2, z = 3 },
+ { x = 7, y = 8, z = 9 },
+ { x = 2, y = 4, z = 8 } ]
diff --git a/internal/toml-test/tests/valid/spec/boolean-0.json b/internal/toml-test/tests/valid/spec/boolean-0.json
new file mode 100644
index 0000000..f956ec0
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/boolean-0.json
@@ -0,0 +1,10 @@
+{
+ "bool1": {
+ "type": "bool",
+ "value": "true"
+ },
+ "bool2": {
+ "type": "bool",
+ "value": "false"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/boolean-0.toml b/internal/toml-test/tests/valid/spec/boolean-0.toml
new file mode 100644
index 0000000..1f80fbd
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/boolean-0.toml
@@ -0,0 +1,2 @@
+bool1 = true
+bool2 = false
diff --git a/internal/toml-test/tests/valid/spec/comment-0.json b/internal/toml-test/tests/valid/spec/comment-0.json
new file mode 100644
index 0000000..d979607
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/comment-0.json
@@ -0,0 +1,10 @@
+{
+ "another": {
+ "type": "string",
+ "value": "# This is not a comment"
+ },
+ "key": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/comment-0.toml b/internal/toml-test/tests/valid/spec/comment-0.toml
new file mode 100644
index 0000000..fae57c6
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/comment-0.toml
@@ -0,0 +1,3 @@
+# This is a full-line comment
+key = "value" # This is a comment at the end of a line
+another = "# This is not a comment"
diff --git a/internal/toml-test/tests/valid/spec/float-0.json b/internal/toml-test/tests/valid/spec/float-0.json
new file mode 100644
index 0000000..ae7d5a5
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-0.json
@@ -0,0 +1,30 @@
+{
+ "flt1": {
+ "type": "float",
+ "value": "1"
+ },
+ "flt2": {
+ "type": "float",
+ "value": "3.1415"
+ },
+ "flt3": {
+ "type": "float",
+ "value": "-0.01"
+ },
+ "flt4": {
+ "type": "float",
+ "value": "5e+22"
+ },
+ "flt5": {
+ "type": "float",
+ "value": "1e+06"
+ },
+ "flt6": {
+ "type": "float",
+ "value": "-0.02"
+ },
+ "flt7": {
+ "type": "float",
+ "value": "6.626e-34"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/float-0.toml b/internal/toml-test/tests/valid/spec/float-0.toml
new file mode 100644
index 0000000..7b29c65
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-0.toml
@@ -0,0 +1,12 @@
+# fractional
+flt1 = +1.0
+flt2 = 3.1415
+flt3 = -0.01
+
+# exponent
+flt4 = 5e+22
+flt5 = 1e06
+flt6 = -2E-2
+
+# both
+flt7 = 6.626e-34
diff --git a/internal/toml-test/tests/valid/spec/float-1.json b/internal/toml-test/tests/valid/spec/float-1.json
new file mode 100644
index 0000000..592a4d2
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-1.json
@@ -0,0 +1,6 @@
+{
+ "flt8": {
+ "type": "float",
+ "value": "224617.445991228"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/float-1.toml b/internal/toml-test/tests/valid/spec/float-1.toml
new file mode 100644
index 0000000..3c8f0aa
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-1.toml
@@ -0,0 +1 @@
+flt8 = 224_617.445_991_228
diff --git a/internal/toml-test/tests/valid/spec/float-2.json b/internal/toml-test/tests/valid/spec/float-2.json
new file mode 100644
index 0000000..9507720
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-2.json
@@ -0,0 +1,26 @@
+{
+ "sf1": {
+ "type": "float",
+ "value": "+Inf"
+ },
+ "sf2": {
+ "type": "float",
+ "value": "+Inf"
+ },
+ "sf3": {
+ "type": "float",
+ "value": "-Inf"
+ },
+ "sf4": {
+ "type": "float",
+ "value": "nan"
+ },
+ "sf5": {
+ "type": "float",
+ "value": "nan"
+ },
+ "sf6": {
+ "type": "float",
+ "value": "nan"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/float-2.toml b/internal/toml-test/tests/valid/spec/float-2.toml
new file mode 100644
index 0000000..4cede55
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/float-2.toml
@@ -0,0 +1,9 @@
+# infinity
+sf1 = inf # positive infinity
+sf2 = +inf # positive infinity
+sf3 = -inf # negative infinity
+
+# not a number
+sf4 = nan # actual sNaN/qNaN encoding is implementation-specific
+sf5 = +nan # same as `nan`
+sf6 = -nan # valid, actual encoding is implementation-specific
diff --git a/internal/toml-test/tests/valid/spec/inline-table-0.json b/internal/toml-test/tests/valid/spec/inline-table-0.json
new file mode 100644
index 0000000..8c0d315
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-0.json
@@ -0,0 +1,30 @@
+{
+ "animal": {
+ "type": {
+ "name": {
+ "type": "string",
+ "value": "pug"
+ }
+ }
+ },
+ "name": {
+ "first": {
+ "type": "string",
+ "value": "Tom"
+ },
+ "last": {
+ "type": "string",
+ "value": "Preston-Werner"
+ }
+ },
+ "point": {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ },
+ "y": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/inline-table-0.toml b/internal/toml-test/tests/valid/spec/inline-table-0.toml
new file mode 100644
index 0000000..26062b5
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-0.toml
@@ -0,0 +1,3 @@
+name = { first = "Tom", last = "Preston-Werner" }
+point = { x = 1, y = 2 }
+animal = { type.name = "pug" }
diff --git a/internal/toml-test/tests/valid/spec/inline-table-1.json b/internal/toml-test/tests/valid/spec/inline-table-1.json
new file mode 100644
index 0000000..8c0d315
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-1.json
@@ -0,0 +1,30 @@
+{
+ "animal": {
+ "type": {
+ "name": {
+ "type": "string",
+ "value": "pug"
+ }
+ }
+ },
+ "name": {
+ "first": {
+ "type": "string",
+ "value": "Tom"
+ },
+ "last": {
+ "type": "string",
+ "value": "Preston-Werner"
+ }
+ },
+ "point": {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ },
+ "y": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/inline-table-1.toml b/internal/toml-test/tests/valid/spec/inline-table-1.toml
new file mode 100644
index 0000000..6f72155
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-1.toml
@@ -0,0 +1,10 @@
+[name]
+first = "Tom"
+last = "Preston-Werner"
+
+[point]
+x = 1
+y = 2
+
+[animal]
+type.name = "pug"
diff --git a/internal/toml-test/tests/valid/spec/inline-table-2.json b/internal/toml-test/tests/valid/spec/inline-table-2.json
new file mode 100644
index 0000000..406691d
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-2.json
@@ -0,0 +1,10 @@
+{
+ "product": {
+ "type": {
+ "name": {
+ "type": "string",
+ "value": "Nail"
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/inline-table-2.toml b/internal/toml-test/tests/valid/spec/inline-table-2.toml
new file mode 100644
index 0000000..aabb500
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-2.toml
@@ -0,0 +1,3 @@
+[product]
+type = { name = "Nail" }
+# type.edible = false # INVALID
diff --git a/internal/toml-test/tests/valid/spec/inline-table-3.json b/internal/toml-test/tests/valid/spec/inline-table-3.json
new file mode 100644
index 0000000..406691d
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-3.json
@@ -0,0 +1,10 @@
+{
+ "product": {
+ "type": {
+ "name": {
+ "type": "string",
+ "value": "Nail"
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/inline-table-3.toml b/internal/toml-test/tests/valid/spec/inline-table-3.toml
new file mode 100644
index 0000000..970949c
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/inline-table-3.toml
@@ -0,0 +1,3 @@
+[product]
+type.name = "Nail"
+# type = { edible = false } # INVALID
diff --git a/internal/toml-test/tests/valid/spec/integer-0.json b/internal/toml-test/tests/valid/spec/integer-0.json
new file mode 100644
index 0000000..72cbbbb
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-0.json
@@ -0,0 +1,18 @@
+{
+ "int1": {
+ "type": "integer",
+ "value": "99"
+ },
+ "int2": {
+ "type": "integer",
+ "value": "42"
+ },
+ "int3": {
+ "type": "integer",
+ "value": "0"
+ },
+ "int4": {
+ "type": "integer",
+ "value": "-17"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/integer-0.toml b/internal/toml-test/tests/valid/spec/integer-0.toml
new file mode 100644
index 0000000..da14ea2
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-0.toml
@@ -0,0 +1,4 @@
+int1 = +99
+int2 = 42
+int3 = 0
+int4 = -17
diff --git a/internal/toml-test/tests/valid/spec/integer-1.json b/internal/toml-test/tests/valid/spec/integer-1.json
new file mode 100644
index 0000000..f431f47
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-1.json
@@ -0,0 +1,18 @@
+{
+ "int5": {
+ "type": "integer",
+ "value": "1000"
+ },
+ "int6": {
+ "type": "integer",
+ "value": "5349221"
+ },
+ "int7": {
+ "type": "integer",
+ "value": "5349221"
+ },
+ "int8": {
+ "type": "integer",
+ "value": "12345"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/integer-1.toml b/internal/toml-test/tests/valid/spec/integer-1.toml
new file mode 100644
index 0000000..0c4b619
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-1.toml
@@ -0,0 +1,4 @@
+int5 = 1_000
+int6 = 5_349_221
+int7 = 53_49_221 # Indian number system grouping
+int8 = 1_2_3_4_5 # VALID but discouraged
diff --git a/internal/toml-test/tests/valid/spec/integer-2.json b/internal/toml-test/tests/valid/spec/integer-2.json
new file mode 100644
index 0000000..b937b2b
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-2.json
@@ -0,0 +1,26 @@
+{
+ "bin1": {
+ "type": "integer",
+ "value": "214"
+ },
+ "hex1": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "hex2": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "hex3": {
+ "type": "integer",
+ "value": "3735928559"
+ },
+ "oct1": {
+ "type": "integer",
+ "value": "342391"
+ },
+ "oct2": {
+ "type": "integer",
+ "value": "493"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/integer-2.toml b/internal/toml-test/tests/valid/spec/integer-2.toml
new file mode 100644
index 0000000..ecffca6
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/integer-2.toml
@@ -0,0 +1,11 @@
+# hexadecimal with prefix `0x`
+hex1 = 0xDEADBEEF
+hex2 = 0xdeadbeef
+hex3 = 0xdead_beef
+
+# octal with prefix `0o`
+oct1 = 0o01234567
+oct2 = 0o755 # useful for Unix file permissions
+
+# binary with prefix `0b`
+bin1 = 0b11010110
diff --git a/internal/toml-test/tests/valid/spec/key-value-pair-0.json b/internal/toml-test/tests/valid/spec/key-value-pair-0.json
new file mode 100644
index 0000000..af3154a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/key-value-pair-0.json
@@ -0,0 +1,6 @@
+{
+ "key": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/key-value-pair-0.toml b/internal/toml-test/tests/valid/spec/key-value-pair-0.toml
new file mode 100644
index 0000000..e5b34eb
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/key-value-pair-0.toml
@@ -0,0 +1 @@
+key = "value"
diff --git a/internal/toml-test/tests/valid/spec/keys-0.json b/internal/toml-test/tests/valid/spec/keys-0.json
new file mode 100644
index 0000000..6130c71
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-0.json
@@ -0,0 +1,18 @@
+{
+ "1234": {
+ "type": "string",
+ "value": "value"
+ },
+ "bare-key": {
+ "type": "string",
+ "value": "value"
+ },
+ "bare_key": {
+ "type": "string",
+ "value": "value"
+ },
+ "key": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-0.toml b/internal/toml-test/tests/valid/spec/keys-0.toml
new file mode 100644
index 0000000..657ff24
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-0.toml
@@ -0,0 +1,4 @@
+key = "value"
+bare_key = "value"
+bare-key = "value"
+1234 = "value"
diff --git a/internal/toml-test/tests/valid/spec/keys-1.json b/internal/toml-test/tests/valid/spec/keys-1.json
new file mode 100644
index 0000000..fb75603
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-1.json
@@ -0,0 +1,22 @@
+{
+ "127.0.0.1": {
+ "type": "string",
+ "value": "value"
+ },
+ "character encoding": {
+ "type": "string",
+ "value": "value"
+ },
+ "key2": {
+ "type": "string",
+ "value": "value"
+ },
+ "quoted \"value\"": {
+ "type": "string",
+ "value": "value"
+ },
+ "ʎǝʞ": {
+ "type": "string",
+ "value": "value"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-1.toml b/internal/toml-test/tests/valid/spec/keys-1.toml
new file mode 100644
index 0000000..cc2d1d0
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-1.toml
@@ -0,0 +1,5 @@
+"127.0.0.1" = "value"
+"character encoding" = "value"
+"ʎǝʞ" = "value"
+'key2' = "value"
+'quoted "value"' = "value"
diff --git a/internal/toml-test/tests/valid/spec/keys-3.json b/internal/toml-test/tests/valid/spec/keys-3.json
new file mode 100644
index 0000000..b8ee719
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-3.json
@@ -0,0 +1,22 @@
+{
+ "name": {
+ "type": "string",
+ "value": "Orange"
+ },
+ "physical": {
+ "color": {
+ "type": "string",
+ "value": "orange"
+ },
+ "shape": {
+ "type": "string",
+ "value": "round"
+ }
+ },
+ "site": {
+ "google.com": {
+ "type": "bool",
+ "value": "true"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-3.toml b/internal/toml-test/tests/valid/spec/keys-3.toml
new file mode 100644
index 0000000..8f83571
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-3.toml
@@ -0,0 +1,4 @@
+name = "Orange"
+physical.color = "orange"
+physical.shape = "round"
+site."google.com" = true
diff --git a/internal/toml-test/tests/valid/spec/keys-4.json b/internal/toml-test/tests/valid/spec/keys-4.json
new file mode 100644
index 0000000..c33966a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-4.json
@@ -0,0 +1,16 @@
+{
+ "fruit": {
+ "color": {
+ "type": "string",
+ "value": "yellow"
+ },
+ "flavor": {
+ "type": "string",
+ "value": "banana"
+ },
+ "name": {
+ "type": "string",
+ "value": "banana"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-4.toml b/internal/toml-test/tests/valid/spec/keys-4.toml
new file mode 100644
index 0000000..75bf90b
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-4.toml
@@ -0,0 +1,3 @@
+fruit.name = "banana" # this is best practice
+fruit. color = "yellow" # same as fruit.color
+fruit . flavor = "banana" # same as fruit.flavor
diff --git a/internal/toml-test/tests/valid/spec/keys-5.json b/internal/toml-test/tests/valid/spec/keys-5.json
new file mode 100644
index 0000000..482b362
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-5.json
@@ -0,0 +1,30 @@
+{
+ "apple": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "skin": {
+ "type": "string",
+ "value": "thin"
+ },
+ "type": {
+ "type": "string",
+ "value": "fruit"
+ }
+ },
+ "orange": {
+ "color": {
+ "type": "string",
+ "value": "orange"
+ },
+ "skin": {
+ "type": "string",
+ "value": "thick"
+ },
+ "type": {
+ "type": "string",
+ "value": "fruit"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-5.toml b/internal/toml-test/tests/valid/spec/keys-5.toml
new file mode 100644
index 0000000..dc94a8c
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-5.toml
@@ -0,0 +1,10 @@
+# VALID BUT DISCOURAGED
+
+apple.type = "fruit"
+orange.type = "fruit"
+
+apple.skin = "thin"
+orange.skin = "thick"
+
+apple.color = "red"
+orange.color = "orange"
diff --git a/internal/toml-test/tests/valid/spec/keys-6.json b/internal/toml-test/tests/valid/spec/keys-6.json
new file mode 100644
index 0000000..482b362
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-6.json
@@ -0,0 +1,30 @@
+{
+ "apple": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "skin": {
+ "type": "string",
+ "value": "thin"
+ },
+ "type": {
+ "type": "string",
+ "value": "fruit"
+ }
+ },
+ "orange": {
+ "color": {
+ "type": "string",
+ "value": "orange"
+ },
+ "skin": {
+ "type": "string",
+ "value": "thick"
+ },
+ "type": {
+ "type": "string",
+ "value": "fruit"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-6.toml b/internal/toml-test/tests/valid/spec/keys-6.toml
new file mode 100644
index 0000000..705686c
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-6.toml
@@ -0,0 +1,9 @@
+# RECOMMENDED
+
+apple.type = "fruit"
+apple.skin = "thin"
+apple.color = "red"
+
+orange.type = "fruit"
+orange.skin = "thick"
+orange.color = "orange"
diff --git a/internal/toml-test/tests/valid/spec/keys-7.json b/internal/toml-test/tests/valid/spec/keys-7.json
new file mode 100644
index 0000000..5ab0e06
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-7.json
@@ -0,0 +1,8 @@
+{
+ "3": {
+ "14159": {
+ "type": "string",
+ "value": "pi"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/keys-7.toml b/internal/toml-test/tests/valid/spec/keys-7.toml
new file mode 100644
index 0000000..2efd659
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/keys-7.toml
@@ -0,0 +1 @@
+3.14159 = "pi"
diff --git a/internal/toml-test/tests/valid/spec/local-date-0.json b/internal/toml-test/tests/valid/spec/local-date-0.json
new file mode 100644
index 0000000..9e281f5
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-date-0.json
@@ -0,0 +1,6 @@
+{
+ "ld1": {
+ "type": "date-local",
+ "value": "1979-05-27"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/local-date-0.toml b/internal/toml-test/tests/valid/spec/local-date-0.toml
new file mode 100644
index 0000000..6670e5d
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-date-0.toml
@@ -0,0 +1 @@
+ld1 = 1979-05-27
diff --git a/internal/toml-test/tests/valid/spec/local-date-time-0.json b/internal/toml-test/tests/valid/spec/local-date-time-0.json
new file mode 100644
index 0000000..d42d49d
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-date-time-0.json
@@ -0,0 +1,10 @@
+{
+ "ldt1": {
+ "type": "datetime-local",
+ "value": "1979-05-27T07:32:00"
+ },
+ "ldt2": {
+ "type": "datetime-local",
+ "value": "1979-05-27T00:32:00.999999"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/local-date-time-0.toml b/internal/toml-test/tests/valid/spec/local-date-time-0.toml
new file mode 100644
index 0000000..38700d2
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-date-time-0.toml
@@ -0,0 +1,2 @@
+ldt1 = 1979-05-27T07:32:00
+ldt2 = 1979-05-27T00:32:00.999999
diff --git a/internal/toml-test/tests/valid/spec/local-time-0.json b/internal/toml-test/tests/valid/spec/local-time-0.json
new file mode 100644
index 0000000..93803fc
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-time-0.json
@@ -0,0 +1,10 @@
+{
+ "lt1": {
+ "type": "time-local",
+ "value": "07:32:00"
+ },
+ "lt2": {
+ "type": "time-local",
+ "value": "00:32:00.999999"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/local-time-0.toml b/internal/toml-test/tests/valid/spec/local-time-0.toml
new file mode 100644
index 0000000..dbd058a
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/local-time-0.toml
@@ -0,0 +1,2 @@
+lt1 = 07:32:00
+lt2 = 00:32:00.999999
diff --git a/internal/toml-test/tests/valid/spec/offset-date-time-0.json b/internal/toml-test/tests/valid/spec/offset-date-time-0.json
new file mode 100644
index 0000000..26d59a8
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/offset-date-time-0.json
@@ -0,0 +1,14 @@
+{
+ "odt1": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00Z"
+ },
+ "odt2": {
+ "type": "datetime",
+ "value": "1979-05-27T00:32:00-07:00"
+ },
+ "odt3": {
+ "type": "datetime",
+ "value": "1979-05-27T00:32:00.999999-07:00"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/offset-date-time-0.toml b/internal/toml-test/tests/valid/spec/offset-date-time-0.toml
new file mode 100644
index 0000000..52eaff2
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/offset-date-time-0.toml
@@ -0,0 +1,3 @@
+odt1 = 1979-05-27T07:32:00Z
+odt2 = 1979-05-27T00:32:00-07:00
+odt3 = 1979-05-27T00:32:00.999999-07:00
diff --git a/internal/toml-test/tests/valid/spec/offset-date-time-1.json b/internal/toml-test/tests/valid/spec/offset-date-time-1.json
new file mode 100644
index 0000000..fa17a4e
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/offset-date-time-1.json
@@ -0,0 +1,6 @@
+{
+ "odt4": {
+ "type": "datetime",
+ "value": "1979-05-27T07:32:00Z"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/offset-date-time-1.toml b/internal/toml-test/tests/valid/spec/offset-date-time-1.toml
new file mode 100644
index 0000000..da9bd3b
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/offset-date-time-1.toml
@@ -0,0 +1 @@
+odt4 = 1979-05-27 07:32:00Z
diff --git a/internal/toml-test/tests/valid/spec/string-0.json b/internal/toml-test/tests/valid/spec/string-0.json
new file mode 100644
index 0000000..3c26fa1
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-0.json
@@ -0,0 +1,6 @@
+{
+ "str": {
+ "type": "string",
+ "value": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-0.toml b/internal/toml-test/tests/valid/spec/string-0.toml
new file mode 100644
index 0000000..b611549
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-0.toml
@@ -0,0 +1 @@
+str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
diff --git a/internal/toml-test/tests/valid/spec/string-1.json b/internal/toml-test/tests/valid/spec/string-1.json
new file mode 100644
index 0000000..316d0f4
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-1.json
@@ -0,0 +1,6 @@
+{
+ "str1": {
+ "type": "string",
+ "value": "Roses are red\nViolets are blue"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-1.toml b/internal/toml-test/tests/valid/spec/string-1.toml
new file mode 100644
index 0000000..337ad08
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-1.toml
@@ -0,0 +1,3 @@
+str1 = """
+Roses are red
+Violets are blue"""
diff --git a/internal/toml-test/tests/valid/spec/string-2.json b/internal/toml-test/tests/valid/spec/string-2.json
new file mode 100644
index 0000000..09faeb6
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-2.json
@@ -0,0 +1,10 @@
+{
+ "str2": {
+ "type": "string",
+ "value": "Roses are red\nViolets are blue"
+ },
+ "str3": {
+ "type": "string",
+ "value": "Roses are red\r\nViolets are blue"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-2.toml b/internal/toml-test/tests/valid/spec/string-2.toml
new file mode 100644
index 0000000..ce5a73b
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-2.toml
@@ -0,0 +1,5 @@
+# On a Unix system, the above multi-line string will most likely be the same as:
+str2 = "Roses are red\nViolets are blue"
+
+# On a Windows system, it will most likely be equivalent to:
+str3 = "Roses are red\r\nViolets are blue"
diff --git a/internal/toml-test/tests/valid/spec/string-3.json b/internal/toml-test/tests/valid/spec/string-3.json
new file mode 100644
index 0000000..c55424f
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-3.json
@@ -0,0 +1,14 @@
+{
+ "str1": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "str2": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "str3": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-3.toml b/internal/toml-test/tests/valid/spec/string-3.toml
new file mode 100644
index 0000000..20b4aa7
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-3.toml
@@ -0,0 +1,15 @@
+# The following strings are byte-for-byte equivalent:
+str1 = "The quick brown fox jumps over the lazy dog."
+
+str2 = """
+The quick brown \
+
+
+ fox jumps over \
+ the lazy dog."""
+
+str3 = """\
+ The quick brown \
+ fox jumps over \
+ the lazy dog.\
+ """
diff --git a/internal/toml-test/tests/valid/spec/string-4.json b/internal/toml-test/tests/valid/spec/string-4.json
new file mode 100644
index 0000000..52ddb19
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-4.json
@@ -0,0 +1,18 @@
+{
+ "str4": {
+ "type": "string",
+ "value": "Here are two quotation marks: \"\". Simple enough."
+ },
+ "str5": {
+ "type": "string",
+ "value": "Here are three quotation marks: \"\"\"."
+ },
+ "str6": {
+ "type": "string",
+ "value": "Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"."
+ },
+ "str7": {
+ "type": "string",
+ "value": "\"This,\" she said, \"is just a pointless statement.\""
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-4.toml b/internal/toml-test/tests/valid/spec/string-4.toml
new file mode 100644
index 0000000..8f51b11
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-4.toml
@@ -0,0 +1,7 @@
+str4 = """Here are two quotation marks: "". Simple enough."""
+# str5 = """Here are three quotation marks: """.""" # INVALID
+str5 = """Here are three quotation marks: ""\"."""
+str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
+
+# "This," she said, "is just a pointless statement."
+str7 = """"This," she said, "is just a pointless statement.""""
diff --git a/internal/toml-test/tests/valid/spec/string-5.json b/internal/toml-test/tests/valid/spec/string-5.json
new file mode 100644
index 0000000..bce72d9
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-5.json
@@ -0,0 +1,18 @@
+{
+ "quoted": {
+ "type": "string",
+ "value": "Tom \"Dubs\" Preston-Werner"
+ },
+ "regex": {
+ "type": "string",
+ "value": "\u003c\\i\\c*\\s*\u003e"
+ },
+ "winpath": {
+ "type": "string",
+ "value": "C:\\Users\\nodejs\\templates"
+ },
+ "winpath2": {
+ "type": "string",
+ "value": "\\\\ServerX\\admin$\\system32\\"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-5.toml b/internal/toml-test/tests/valid/spec/string-5.toml
new file mode 100644
index 0000000..36772bb
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-5.toml
@@ -0,0 +1,5 @@
+# What you see is what you get.
+winpath = 'C:\Users\nodejs\templates'
+winpath2 = '\\ServerX\admin$\system32\'
+quoted = 'Tom "Dubs" Preston-Werner'
+regex = '<\i\c*\s*>'
diff --git a/internal/toml-test/tests/valid/spec/string-6.json b/internal/toml-test/tests/valid/spec/string-6.json
new file mode 100644
index 0000000..aa7663e
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-6.json
@@ -0,0 +1,10 @@
+{
+ "lines": {
+ "type": "string",
+ "value": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n"
+ },
+ "regex2": {
+ "type": "string",
+ "value": "I [dw]on't need \\d{2} apples"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-6.toml b/internal/toml-test/tests/valid/spec/string-6.toml
new file mode 100644
index 0000000..bc88494
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-6.toml
@@ -0,0 +1,7 @@
+regex2 = '''I [dw]on't need \d{2} apples'''
+lines = '''
+The first newline is
+trimmed in raw strings.
+ All other whitespace
+ is preserved.
+'''
diff --git a/internal/toml-test/tests/valid/spec/string-7.json b/internal/toml-test/tests/valid/spec/string-7.json
new file mode 100644
index 0000000..ad0fff2
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-7.json
@@ -0,0 +1,14 @@
+{
+ "apos15": {
+ "type": "string",
+ "value": "Here are fifteen apostrophes: '''''''''''''''"
+ },
+ "quot15": {
+ "type": "string",
+ "value": "Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\""
+ },
+ "str": {
+ "type": "string",
+ "value": "'That,' she said, 'is still pointless.'"
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/string-7.toml b/internal/toml-test/tests/valid/spec/string-7.toml
new file mode 100644
index 0000000..82890b0
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/string-7.toml
@@ -0,0 +1,7 @@
+quot15 = '''Here are fifteen quotation marks: """""""""""""""'''
+
+# apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
+apos15 = "Here are fifteen apostrophes: '''''''''''''''"
+
+# 'That,' she said, 'is still pointless.'
+str = ''''That,' she said, 'is still pointless.''''
diff --git a/internal/toml-test/tests/valid/spec/table-0.json b/internal/toml-test/tests/valid/spec/table-0.json
new file mode 100644
index 0000000..b6e7fb5
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-0.json
@@ -0,0 +1,3 @@
+{
+ "table": {}
+}
diff --git a/internal/toml-test/tests/valid/spec/table-0.toml b/internal/toml-test/tests/valid/spec/table-0.toml
new file mode 100644
index 0000000..f1098fd
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-0.toml
@@ -0,0 +1 @@
+[table]
diff --git a/internal/toml-test/tests/valid/spec/table-1.json b/internal/toml-test/tests/valid/spec/table-1.json
new file mode 100644
index 0000000..f8521e0
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-1.json
@@ -0,0 +1,22 @@
+{
+ "table-1": {
+ "key1": {
+ "type": "string",
+ "value": "some string"
+ },
+ "key2": {
+ "type": "integer",
+ "value": "123"
+ }
+ },
+ "table-2": {
+ "key1": {
+ "type": "string",
+ "value": "another string"
+ },
+ "key2": {
+ "type": "integer",
+ "value": "456"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-1.toml b/internal/toml-test/tests/valid/spec/table-1.toml
new file mode 100644
index 0000000..6b4884c
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-1.toml
@@ -0,0 +1,7 @@
+[table-1]
+key1 = "some string"
+key2 = 123
+
+[table-2]
+key1 = "another string"
+key2 = 456
diff --git a/internal/toml-test/tests/valid/spec/table-2.json b/internal/toml-test/tests/valid/spec/table-2.json
new file mode 100644
index 0000000..afb74d5
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-2.json
@@ -0,0 +1,12 @@
+{
+ "dog": {
+ "tater.man": {
+ "type": {
+ "name": {
+ "type": "string",
+ "value": "pug"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-2.toml b/internal/toml-test/tests/valid/spec/table-2.toml
new file mode 100644
index 0000000..32cbe79
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-2.toml
@@ -0,0 +1,2 @@
+[dog."tater.man"]
+type.name = "pug"
diff --git a/internal/toml-test/tests/valid/spec/table-3.json b/internal/toml-test/tests/valid/spec/table-3.json
new file mode 100644
index 0000000..0e40e3f
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-3.json
@@ -0,0 +1,22 @@
+{
+ "a": {
+ "b": {
+ "c": {}
+ }
+ },
+ "d": {
+ "e": {
+ "f": {}
+ }
+ },
+ "g": {
+ "h": {
+ "i": {}
+ }
+ },
+ "j": {
+ "ʞ": {
+ "l": {}
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-3.toml b/internal/toml-test/tests/valid/spec/table-3.toml
new file mode 100644
index 0000000..0b10db3
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-3.toml
@@ -0,0 +1,4 @@
+[a.b.c] # this is best practice
+[ d.e.f ] # same as [d.e.f]
+[ g . h . i ] # same as [g.h.i]
+[ j . "ʞ" . 'l' ] # same as [j."ʞ".'l']
diff --git a/internal/toml-test/tests/valid/spec/table-4.json b/internal/toml-test/tests/valid/spec/table-4.json
new file mode 100644
index 0000000..3c390c6
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-4.json
@@ -0,0 +1,9 @@
+{
+ "x": {
+ "y": {
+ "z": {
+ "w": {}
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-4.toml b/internal/toml-test/tests/valid/spec/table-4.toml
new file mode 100644
index 0000000..256e6c0
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-4.toml
@@ -0,0 +1,6 @@
+# [x] you
+# [x.y] don't
+# [x.y.z] need these
+[x.y.z.w] # for this to work
+
+[x] # defining a super-table afterward is ok
diff --git a/internal/toml-test/tests/valid/spec/table-5.json b/internal/toml-test/tests/valid/spec/table-5.json
new file mode 100644
index 0000000..2ec8407
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-5.json
@@ -0,0 +1,7 @@
+{
+ "animal": {},
+ "fruit": {
+ "apple": {},
+ "orange": {}
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-5.toml b/internal/toml-test/tests/valid/spec/table-5.toml
new file mode 100644
index 0000000..03fe3a3
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-5.toml
@@ -0,0 +1,4 @@
+# VALID BUT DISCOURAGED
+[fruit.apple]
+[animal]
+[fruit.orange]
diff --git a/internal/toml-test/tests/valid/spec/table-6.json b/internal/toml-test/tests/valid/spec/table-6.json
new file mode 100644
index 0000000..2ec8407
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-6.json
@@ -0,0 +1,7 @@
+{
+ "animal": {},
+ "fruit": {
+ "apple": {},
+ "orange": {}
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-6.toml b/internal/toml-test/tests/valid/spec/table-6.toml
new file mode 100644
index 0000000..ef330e4
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-6.toml
@@ -0,0 +1,4 @@
+# RECOMMENDED
+[fruit.apple]
+[fruit.orange]
+[animal]
diff --git a/internal/toml-test/tests/valid/spec/table-7.json b/internal/toml-test/tests/valid/spec/table-7.json
new file mode 100644
index 0000000..3bdfe95
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-7.json
@@ -0,0 +1,20 @@
+{
+ "breed": {
+ "type": "string",
+ "value": "pug"
+ },
+ "name": {
+ "type": "string",
+ "value": "Fido"
+ },
+ "owner": {
+ "member_since": {
+ "type": "date-local",
+ "value": "1999-08-04"
+ },
+ "name": {
+ "type": "string",
+ "value": "Regina Dogman"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-7.toml b/internal/toml-test/tests/valid/spec/table-7.toml
new file mode 100644
index 0000000..c57a229
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-7.toml
@@ -0,0 +1,8 @@
+# Top-level table begins.
+name = "Fido"
+breed = "pug"
+
+# Top-level table ends.
+[owner]
+name = "Regina Dogman"
+member_since = 1999-08-04
diff --git a/internal/toml-test/tests/valid/spec/table-8.json b/internal/toml-test/tests/valid/spec/table-8.json
new file mode 100644
index 0000000..dd59a51
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-8.json
@@ -0,0 +1,16 @@
+{
+ "fruit": {
+ "apple": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "taste": {
+ "sweet": {
+ "type": "bool",
+ "value": "true"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-8.toml b/internal/toml-test/tests/valid/spec/table-8.toml
new file mode 100644
index 0000000..d5a2c11
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-8.toml
@@ -0,0 +1,7 @@
+fruit.apple.color = "red"
+# Defines a table named fruit
+# Defines a table named fruit.apple
+
+fruit.apple.taste.sweet = true
+# Defines a table named fruit.apple.taste
+# fruit and fruit.apple were already created
diff --git a/internal/toml-test/tests/valid/spec/table-9.json b/internal/toml-test/tests/valid/spec/table-9.json
new file mode 100644
index 0000000..89e8d5e
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-9.json
@@ -0,0 +1,22 @@
+{
+ "fruit": {
+ "apple": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "taste": {
+ "sweet": {
+ "type": "bool",
+ "value": "true"
+ }
+ },
+ "texture": {
+ "smooth": {
+ "type": "bool",
+ "value": "true"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/spec/table-9.toml b/internal/toml-test/tests/valid/spec/table-9.toml
new file mode 100644
index 0000000..ae2d65b
--- /dev/null
+++ b/internal/toml-test/tests/valid/spec/table-9.toml
@@ -0,0 +1,9 @@
+[fruit]
+apple.color = "red"
+apple.taste.sweet = true
+
+# [fruit.apple] # INVALID
+# [fruit.apple.taste] # INVALID
+
+[fruit.apple.texture] # you can add sub-tables
+smooth = true
diff --git a/internal/toml-test/tests/valid/string/double-quote-escape.json b/internal/toml-test/tests/valid/string/double-quote-escape.json
new file mode 100644
index 0000000..0c4ac37
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/double-quote-escape.json
@@ -0,0 +1,6 @@
+{
+ "test": {
+ "type": "string",
+ "value": "\"one\""
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/double-quote-escape.toml b/internal/toml-test/tests/valid/string/double-quote-escape.toml
new file mode 100644
index 0000000..78e7e72
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/double-quote-escape.toml
@@ -0,0 +1 @@
+test = "\"one\""
diff --git a/internal/toml-test/tests/valid/string/empty.json b/internal/toml-test/tests/valid/string/empty.json
new file mode 100644
index 0000000..efe96c3
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/empty.json
@@ -0,0 +1,6 @@
+{
+ "answer": {
+ "type": "string",
+ "value": ""
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/empty.toml b/internal/toml-test/tests/valid/string/empty.toml
new file mode 100644
index 0000000..e37e681
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/empty.toml
@@ -0,0 +1 @@
+answer = ""
diff --git a/internal/toml-test/tests/valid/string/escape-esc.json b/internal/toml-test/tests/valid/string/escape-esc.json
new file mode 100644
index 0000000..be39241
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escape-esc.json
@@ -0,0 +1,6 @@
+{
+ "esc": {
+ "type": "string",
+ "value": "\u001b There is no escape! \u001b"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/escape-esc.toml b/internal/toml-test/tests/valid/string/escape-esc.toml
new file mode 100644
index 0000000..1e158fe
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escape-esc.toml
@@ -0,0 +1 @@
+esc = "\e There is no escape! \e"
diff --git a/internal/toml-test/tests/valid/string/escape-tricky.json b/internal/toml-test/tests/valid/string/escape-tricky.json
new file mode 100644
index 0000000..572e0a5
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escape-tricky.json
@@ -0,0 +1,30 @@
+{
+ "end_esc": {
+ "type": "string",
+ "value": "String does not end here\" but ends here\\"
+ },
+ "lit_end_esc": {
+ "type": "string",
+ "value": "String ends here\\"
+ },
+ "lit_multiline_end": {
+ "type": "string",
+ "value": "There is no escape\\"
+ },
+ "lit_multiline_not_unicode": {
+ "type": "string",
+ "value": "\\u007f"
+ },
+ "multiline_end_esc": {
+ "type": "string",
+ "value": "When will it end? \"\"\"...\"\"\" should be here\""
+ },
+ "multiline_not_unicode": {
+ "type": "string",
+ "value": "\\u0041"
+ },
+ "multiline_unicode": {
+ "type": "string",
+ "value": " "
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/escape-tricky.toml b/internal/toml-test/tests/valid/string/escape-tricky.toml
new file mode 100644
index 0000000..dc204cb
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escape-tricky.toml
@@ -0,0 +1,15 @@
+end_esc = "String does not end here\" but ends here\\"
+lit_end_esc = 'String ends here\'
+
+multiline_unicode = """
+\u00a0"""
+
+multiline_not_unicode = """
+\\u0041"""
+
+multiline_end_esc = """When will it end? \"""...""\" should be here\""""
+
+lit_multiline_not_unicode = '''
+\u007f'''
+
+lit_multiline_end = '''There is no escape\'''
diff --git a/internal/toml-test/tests/valid/string/escaped-escape.json b/internal/toml-test/tests/valid/string/escaped-escape.json
new file mode 100644
index 0000000..112c0c9
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escaped-escape.json
@@ -0,0 +1,6 @@
+{
+ "answer": {
+ "type": "string",
+ "value": "\\x64"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/escaped-escape.toml b/internal/toml-test/tests/valid/string/escaped-escape.toml
new file mode 100644
index 0000000..d575876
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escaped-escape.toml
@@ -0,0 +1 @@
+answer = "\\x64"
diff --git a/internal/toml-test/tests/valid/string/escapes.json b/internal/toml-test/tests/valid/string/escapes.json
new file mode 100644
index 0000000..d849574
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escapes.json
@@ -0,0 +1,54 @@
+{
+ "backslash": {
+ "type": "string",
+ "value": "This string has a \\ backslash character."
+ },
+ "backspace": {
+ "type": "string",
+ "value": "This string has a \u0008 backspace character."
+ },
+ "carriage": {
+ "type": "string",
+ "value": "This string has a \r carriage return character."
+ },
+ "delete": {
+ "type": "string",
+ "value": "This string has a  delete control code."
+ },
+ "formfeed": {
+ "type": "string",
+ "value": "This string has a \u000c form feed character."
+ },
+ "newline": {
+ "type": "string",
+ "value": "This string has a \n new line character."
+ },
+ "notunicode1": {
+ "type": "string",
+ "value": "This string does not have a unicode \\u escape."
+ },
+ "notunicode2": {
+ "type": "string",
+ "value": "This string does not have a unicode \\u escape."
+ },
+ "notunicode3": {
+ "type": "string",
+ "value": "This string does not have a unicode \\u0075 escape."
+ },
+ "notunicode4": {
+ "type": "string",
+ "value": "This string does not have a unicode \\u escape."
+ },
+ "quote": {
+ "type": "string",
+ "value": "This string has a \" quote character."
+ },
+ "tab": {
+ "type": "string",
+ "value": "This string has a \t tab character."
+ },
+ "unitseparator": {
+ "type": "string",
+ "value": "This string has a \u001f unit separator control code."
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/escapes.toml b/internal/toml-test/tests/valid/string/escapes.toml
new file mode 100644
index 0000000..78f7ebd
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/escapes.toml
@@ -0,0 +1,13 @@
+backspace = "This string has a \b backspace character."
+tab = "This string has a \t tab character."
+newline = "This string has a \n new line character."
+formfeed = "This string has a \f form feed character."
+carriage = "This string has a \r carriage return character."
+quote = "This string has a \" quote character."
+backslash = "This string has a \\ backslash character."
+notunicode1 = "This string does not have a unicode \\u escape."
+notunicode2 = "This string does not have a unicode \u005Cu escape."
+notunicode3 = "This string does not have a unicode \\u0075 escape."
+notunicode4 = "This string does not have a unicode \\\u0075 escape."
+delete = "This string has a \u007F delete control code."
+unitseparator = "This string has a \u001F unit separator control code."
diff --git a/internal/toml-test/tests/valid/string/hex-escape.json b/internal/toml-test/tests/valid/string/hex-escape.json
new file mode 100644
index 0000000..0eae986
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/hex-escape.json
@@ -0,0 +1,34 @@
+{
+ "bs": {
+ "type": "string",
+ "value": ""
+ },
+ "hello": {
+ "type": "string",
+ "value": "hello\n"
+ },
+ "higher-than-127": {
+ "type": "string",
+ "value": "Sørmirbæren"
+ },
+ "literal": {
+ "type": "string",
+ "value": "\\x20 \\x09 \\x0d\\x0a"
+ },
+ "multiline": {
+ "type": "string",
+ "value": " \t \u001b \r\n\n\n\u0000\nhello\n\nSørmirbæren\n"
+ },
+ "multiline-literal": {
+ "type": "string",
+ "value": "\\x20 \\x09 \\x0d\\x0a\n"
+ },
+ "nul": {
+ "type": "string",
+ "value": "\u0000"
+ },
+ "whitespace": {
+ "type": "string",
+ "value": " \t \u001b \r\n"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/hex-escape.toml b/internal/toml-test/tests/valid/string/hex-escape.toml
new file mode 100644
index 0000000..26d1668
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/hex-escape.toml
@@ -0,0 +1,21 @@
+# \x for the first 255 codepoints
+
+whitespace = "\x20 \x09 \x1b \x0d\x0a"
+bs = "\x7f"
+nul = "\x00"
+hello = "\x68\x65\x6c\x6c\x6f\x0a"
+higher-than-127 = "S\xf8rmirb\xe6ren"
+
+multiline = """
+\x20 \x09 \x1b \x0d\x0a
+\x7f
+\x00
+\x68\x65\x6c\x6c\x6f\x0a
+\x53\xF8\x72\x6D\x69\x72\x62\xE6\x72\x65\x6E
+"""
+
+# Not inside literals.
+literal = '\x20 \x09 \x0d\x0a'
+multiline-literal = '''
+\x20 \x09 \x0d\x0a
+'''
diff --git a/internal/toml-test/tests/valid/string/multiline-escaped-crlf.json b/internal/toml-test/tests/valid/string/multiline-escaped-crlf.json
new file mode 100644
index 0000000..aa5e3ee
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline-escaped-crlf.json
@@ -0,0 +1,6 @@
+{
+ "0": {
+ "type": "string",
+ "value": ""
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/multiline-escaped-crlf.toml b/internal/toml-test/tests/valid/string/multiline-escaped-crlf.toml
new file mode 100644
index 0000000..97d697d
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline-escaped-crlf.toml
@@ -0,0 +1,4 @@
+# The following line should be an unescaped backslash followed by a Windows
+# newline sequence ("\r\n")
+0="""\
+"""
diff --git a/internal/toml-test/tests/valid/string/multiline-quotes.json b/internal/toml-test/tests/valid/string/multiline-quotes.json
new file mode 100644
index 0000000..af9c4c8
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline-quotes.json
@@ -0,0 +1,46 @@
+{
+ "escaped": {
+ "type": "string",
+ "value": "lol\"\"\""
+ },
+ "lit_one": {
+ "type": "string",
+ "value": "'one quote'"
+ },
+ "lit_one_space": {
+ "type": "string",
+ "value": " 'one quote' "
+ },
+ "lit_two": {
+ "type": "string",
+ "value": "''two quotes''"
+ },
+ "lit_two_space": {
+ "type": "string",
+ "value": " ''two quotes'' "
+ },
+ "mismatch1": {
+ "type": "string",
+ "value": "aaa'''bbb"
+ },
+ "mismatch2": {
+ "type": "string",
+ "value": "aaa\"\"\"bbb"
+ },
+ "one": {
+ "type": "string",
+ "value": "\"one quote\""
+ },
+ "one_space": {
+ "type": "string",
+ "value": " \"one quote\" "
+ },
+ "two": {
+ "type": "string",
+ "value": "\"\"two quotes\"\""
+ },
+ "two_space": {
+ "type": "string",
+ "value": " \"\"two quotes\"\" "
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/multiline-quotes.toml b/internal/toml-test/tests/valid/string/multiline-quotes.toml
new file mode 100644
index 0000000..085e127
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline-quotes.toml
@@ -0,0 +1,19 @@
+# Make sure that quotes inside multiline strings are allowed, including right
+# after the opening '''/""" and before the closing '''/"""
+
+lit_one = ''''one quote''''
+lit_two = '''''two quotes'''''
+lit_one_space = ''' 'one quote' '''
+lit_two_space = ''' ''two quotes'' '''
+
+one = """"one quote""""
+two = """""two quotes"""""
+one_space = """ "one quote" """
+two_space = """ ""two quotes"" """
+
+mismatch1 = """aaa'''bbb"""
+mismatch2 = '''aaa"""bbb'''
+
+# Three opening """, then one escaped ", then two "" (allowed), and then three
+# closing """
+escaped = """lol\""""""
diff --git a/internal/toml-test/tests/valid/string/multiline.json b/internal/toml-test/tests/valid/string/multiline.json
new file mode 100644
index 0000000..bb0c47b
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline.json
@@ -0,0 +1,58 @@
+{
+ "equivalent_one": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "equivalent_three": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "equivalent_two": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "escape-bs-1": {
+ "type": "string",
+ "value": "a \\\nb"
+ },
+ "escape-bs-2": {
+ "type": "string",
+ "value": "a \\b"
+ },
+ "escape-bs-3": {
+ "type": "string",
+ "value": "a \\\\\n b"
+ },
+ "keep-ws-before": {
+ "type": "string",
+ "value": "a \tb"
+ },
+ "multiline_empty_four": {
+ "type": "string",
+ "value": ""
+ },
+ "multiline_empty_one": {
+ "type": "string",
+ "value": ""
+ },
+ "multiline_empty_three": {
+ "type": "string",
+ "value": ""
+ },
+ "multiline_empty_two": {
+ "type": "string",
+ "value": ""
+ },
+ "no-space": {
+ "type": "string",
+ "value": "ab"
+ },
+ "whitespace-after-bs": {
+ "type": "string",
+ "value": "The quick brown fox jumps over the lazy dog."
+ },
+ "only-ignore-first": {
+ "type": "string",
+ "value": "Here are two\nlines of text.\nAnd another\n two.\n"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/multiline.toml b/internal/toml-test/tests/valid/string/multiline.toml
new file mode 100644
index 0000000..4f2fd00
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/multiline.toml
@@ -0,0 +1,62 @@
+# NOTE: this file includes some literal tab characters.
+
+multiline_empty_one = """"""
+
+# A newline immediately following the opening delimiter will be trimmed.
+multiline_empty_two = """
+"""
+
+# \ at the end of line trims newlines as well; note that last \ is followed by
+# two spaces, which are ignored.
+multiline_empty_three = """\
+ """
+multiline_empty_four = """\
+ \
+ \
+ """
+
+equivalent_one = "The quick brown fox jumps over the lazy dog."
+equivalent_two = """
+The quick brown \
+
+
+ fox jumps over \
+ the lazy dog."""
+
+equivalent_three = """\
+ The quick brown \
+ fox jumps over \
+ the lazy dog.\
+ """
+
+whitespace-after-bs = """\
+ The quick brown \
+ fox jumps over \
+ the lazy dog.\
+ """
+
+only-ignore-first = """
+Here are \
+ two
+lines of text.
+And \
+
+ another
+ two.
+"""
+
+no-space = """a\
+ b"""
+
+# Has tab character.
+keep-ws-before = """a \
+ b"""
+
+escape-bs-1 = """a \\
+b"""
+
+escape-bs-2 = """a \\\
+b"""
+
+escape-bs-3 = """a \\\\
+ b"""
diff --git a/internal/toml-test/tests/valid/string/nl.json b/internal/toml-test/tests/valid/string/nl.json
new file mode 100644
index 0000000..8eae09a
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/nl.json
@@ -0,0 +1,22 @@
+{
+ "lit_nl_end": {
+ "type": "string",
+ "value": "value\\n"
+ },
+ "lit_nl_mid": {
+ "type": "string",
+ "value": "val\\nue"
+ },
+ "lit_nl_uni": {
+ "type": "string",
+ "value": "val\\ue"
+ },
+ "nl_end": {
+ "type": "string",
+ "value": "value\n"
+ },
+ "nl_mid": {
+ "type": "string",
+ "value": "val\nue"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/nl.toml b/internal/toml-test/tests/valid/string/nl.toml
new file mode 100644
index 0000000..1e09a8b
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/nl.toml
@@ -0,0 +1,6 @@
+nl_mid = "val\nue"
+nl_end = """value\n"""
+
+lit_nl_end = '''value\n'''
+lit_nl_mid = 'val\nue'
+lit_nl_uni = 'val\ue'
diff --git a/internal/toml-test/tests/valid/string/quoted-unicode.json b/internal/toml-test/tests/valid/string/quoted-unicode.json
new file mode 100644
index 0000000..9c6cc95
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/quoted-unicode.json
@@ -0,0 +1,18 @@
+{
+ "escaped_string": {
+ "type": "string",
+ "value": "\u0000 \b \f A \u007f \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff"
+ },
+ "not_escaped_string": {
+ "type": "string",
+ "value": "\\u0000 \\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff"
+ },
+ "basic_string": {
+ "type": "string",
+ "value": "~ \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff"
+ },
+ "literal_string": {
+ "type": "string",
+ "value": "~ \u0080 \u00ff \ud7ff \ue000 \uffff \ud800\udc00 \udbff\udfff"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/quoted-unicode.toml b/internal/toml-test/tests/valid/string/quoted-unicode.toml
new file mode 100644
index 0000000..3dfb147
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/quoted-unicode.toml
@@ -0,0 +1,6 @@
+
+escaped_string = "\u0000 \u0008 \u000c \U00000041 \u007f \u0080 \u00ff \ud7ff \ue000 \uffff \U00010000 \U0010ffff"
+not_escaped_string = '\u0000 \u0008 \u000c \U00000041 \u007f \u0080 \u00ff \ud7ff \ue000 \uffff \U00010000 \U0010ffff'
+
+basic_string = "~ € ÿ ퟿  ￿ 𐀀 􏿿"
+literal_string = '~ € ÿ ퟿  ￿ 𐀀 􏿿'
diff --git a/internal/toml-test/tests/valid/string/raw-multiline.json b/internal/toml-test/tests/valid/string/raw-multiline.json
new file mode 100644
index 0000000..4159eb0
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/raw-multiline.json
@@ -0,0 +1,18 @@
+{
+ "firstnl": {
+ "type": "string",
+ "value": "This string has a ' quote character."
+ },
+ "multiline": {
+ "type": "string",
+ "value": "This string\nhas ' a quote character\nand more than\none newline\nin it."
+ },
+ "oneline": {
+ "type": "string",
+ "value": "This string has a ' quote character."
+ },
+ "multiline_with_tab": {
+ "type": "string",
+ "value": "First line\n\t Followed by a tab"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/raw-multiline.toml b/internal/toml-test/tests/valid/string/raw-multiline.toml
new file mode 100644
index 0000000..fff00d9
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/raw-multiline.toml
@@ -0,0 +1,18 @@
+# Single ' should be allowed.
+oneline = '''This string has a ' quote character.'''
+
+# A newline immediately following the opening delimiter will be trimmed.
+firstnl = '''
+This string has a ' quote character.'''
+
+# All other whitespace and newline characters remain intact.
+multiline = '''
+This string
+has ' a quote character
+and more than
+one newline
+in it.'''
+
+# Tab character in literal string does not need to be escaped
+multiline_with_tab = '''First line
+ Followed by a tab'''
diff --git a/internal/toml-test/tests/valid/string/raw.json b/internal/toml-test/tests/valid/string/raw.json
new file mode 100644
index 0000000..3e1cfd1
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/raw.json
@@ -0,0 +1,34 @@
+{
+ "backslash": {
+ "type": "string",
+ "value": "This string has a \\\\ backslash character."
+ },
+ "backspace": {
+ "type": "string",
+ "value": "This string has a \\b backspace character."
+ },
+ "carriage": {
+ "type": "string",
+ "value": "This string has a \\r carriage return character."
+ },
+ "formfeed": {
+ "type": "string",
+ "value": "This string has a \\f form feed character."
+ },
+ "newline": {
+ "type": "string",
+ "value": "This string has a \\n new line character."
+ },
+ "slash": {
+ "type": "string",
+ "value": "This string has a \\/ slash character."
+ },
+ "tab": {
+ "type": "string",
+ "value": "This string has a \\t tab character."
+ },
+ "unescaped_tab": {
+ "type": "string",
+ "value": "This string has an \t unescaped tab character."
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/raw.toml b/internal/toml-test/tests/valid/string/raw.toml
new file mode 100644
index 0000000..993307f
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/raw.toml
@@ -0,0 +1,8 @@
+backspace = 'This string has a \b backspace character.'
+tab = 'This string has a \t tab character.'
+unescaped_tab = 'This string has an unescaped tab character.'
+newline = 'This string has a \n new line character.'
+formfeed = 'This string has a \f form feed character.'
+carriage = 'This string has a \r carriage return character.'
+slash = 'This string has a \/ slash character.'
+backslash = 'This string has a \\ backslash character.'
diff --git a/internal/toml-test/tests/valid/string/simple.json b/internal/toml-test/tests/valid/string/simple.json
new file mode 100644
index 0000000..2561cda
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/simple.json
@@ -0,0 +1,6 @@
+{
+ "answer": {
+ "type": "string",
+ "value": "You are not drinking enough whisky."
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/simple.toml b/internal/toml-test/tests/valid/string/simple.toml
new file mode 100644
index 0000000..e17ade6
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/simple.toml
@@ -0,0 +1 @@
+answer = "You are not drinking enough whisky."
diff --git a/internal/toml-test/tests/valid/string/unicode-escape.json b/internal/toml-test/tests/valid/string/unicode-escape.json
new file mode 100644
index 0000000..9ae728b
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/unicode-escape.json
@@ -0,0 +1,10 @@
+{
+ "answer4": {
+ "type": "string",
+ "value": "δ"
+ },
+ "answer8": {
+ "type": "string",
+ "value": "δ"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/unicode-escape.toml b/internal/toml-test/tests/valid/string/unicode-escape.toml
new file mode 100644
index 0000000..82faecb
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/unicode-escape.toml
@@ -0,0 +1,2 @@
+answer4 = "\u03B4"
+answer8 = "\U000003B4"
diff --git a/internal/toml-test/tests/valid/string/unicode-literal.json b/internal/toml-test/tests/valid/string/unicode-literal.json
new file mode 100644
index 0000000..7f15df4
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/unicode-literal.json
@@ -0,0 +1,6 @@
+{
+ "answer": {
+ "type": "string",
+ "value": "δ"
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/unicode-literal.toml b/internal/toml-test/tests/valid/string/unicode-literal.toml
new file mode 100644
index 0000000..c65723c
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/unicode-literal.toml
@@ -0,0 +1 @@
+answer = "δ"
diff --git a/internal/toml-test/tests/valid/string/with-pound.json b/internal/toml-test/tests/valid/string/with-pound.json
new file mode 100644
index 0000000..2cb8beb
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/with-pound.json
@@ -0,0 +1,10 @@
+{
+ "pound": {
+ "type": "string",
+ "value": "We see no # comments here."
+ },
+ "poundcomment": {
+ "type": "string",
+ "value": "But there are # some comments here."
+ }
+}
diff --git a/internal/toml-test/tests/valid/string/with-pound.toml b/internal/toml-test/tests/valid/string/with-pound.toml
new file mode 100644
index 0000000..5fd8746
--- /dev/null
+++ b/internal/toml-test/tests/valid/string/with-pound.toml
@@ -0,0 +1,2 @@
+pound = "We see no # comments here."
+poundcomment = "But there are # some comments here." # Did I # mess you up?
diff --git a/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.json b/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.json
new file mode 100644
index 0000000..de156c5
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.json
@@ -0,0 +1,16 @@
+{
+ "a": {
+ "b": [
+ {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+ ],
+ "y": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.toml b/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.toml
new file mode 100644
index 0000000..bab8d61
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-implicit-and-explicit-after.toml
@@ -0,0 +1,5 @@
+[[a.b]]
+x = 1
+
+[a]
+y = 2
diff --git a/internal/toml-test/tests/valid/table/array-implicit.json b/internal/toml-test/tests/valid/table/array-implicit.json
new file mode 100644
index 0000000..ee164c5
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-implicit.json
@@ -0,0 +1,12 @@
+{
+ "albums": {
+ "songs": [
+ {
+ "name": {
+ "type": "string",
+ "value": "Glory Days"
+ }
+ }
+ ]
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/array-implicit.toml b/internal/toml-test/tests/valid/table/array-implicit.toml
new file mode 100644
index 0000000..3157ac9
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-implicit.toml
@@ -0,0 +1,2 @@
+[[albums.songs]]
+name = "Glory Days"
diff --git a/internal/toml-test/tests/valid/table/array-many.json b/internal/toml-test/tests/valid/table/array-many.json
new file mode 100644
index 0000000..fe78bf6
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-many.json
@@ -0,0 +1,34 @@
+{
+ "people": [
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Bruce"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Springsteen"
+ }
+ },
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Eric"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Clapton"
+ }
+ },
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Bob"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Seger"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/table/array-many.toml b/internal/toml-test/tests/valid/table/array-many.toml
new file mode 100644
index 0000000..46062be
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-many.toml
@@ -0,0 +1,11 @@
+[[people]]
+first_name = "Bruce"
+last_name = "Springsteen"
+
+[[people]]
+first_name = "Eric"
+last_name = "Clapton"
+
+[[people]]
+first_name = "Bob"
+last_name = "Seger"
diff --git a/internal/toml-test/tests/valid/table/array-nest.json b/internal/toml-test/tests/valid/table/array-nest.json
new file mode 100644
index 0000000..97f55c8
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-nest.json
@@ -0,0 +1,44 @@
+{
+ "albums": [
+ {
+ "name": {
+ "type": "string",
+ "value": "Born to Run"
+ },
+ "songs": [
+ {
+ "name": {
+ "type": "string",
+ "value": "Jungleland"
+ }
+ },
+ {
+ "name": {
+ "type": "string",
+ "value": "Meeting Across the River"
+ }
+ }
+ ]
+ },
+ {
+ "name": {
+ "type": "string",
+ "value": "Born in the USA"
+ },
+ "songs": [
+ {
+ "name": {
+ "type": "string",
+ "value": "Glory Days"
+ }
+ },
+ {
+ "name": {
+ "type": "string",
+ "value": "Dancing in the Dark"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/table/array-nest.toml b/internal/toml-test/tests/valid/table/array-nest.toml
new file mode 100644
index 0000000..d659a3d
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-nest.toml
@@ -0,0 +1,17 @@
+[[albums]]
+name = "Born to Run"
+
+ [[albums.songs]]
+ name = "Jungleland"
+
+ [[albums.songs]]
+ name = "Meeting Across the River"
+
+[[albums]]
+name = "Born in the USA"
+
+ [[albums.songs]]
+ name = "Glory Days"
+
+ [[albums.songs]]
+ name = "Dancing in the Dark"
diff --git a/internal/toml-test/tests/valid/table/array-one.json b/internal/toml-test/tests/valid/table/array-one.json
new file mode 100644
index 0000000..5cd67de
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-one.json
@@ -0,0 +1,14 @@
+{
+ "people": [
+ {
+ "first_name": {
+ "type": "string",
+ "value": "Bruce"
+ },
+ "last_name": {
+ "type": "string",
+ "value": "Springsteen"
+ }
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/table/array-one.toml b/internal/toml-test/tests/valid/table/array-one.toml
new file mode 100644
index 0000000..cd7e1b6
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-one.toml
@@ -0,0 +1,3 @@
+[[people]]
+first_name = "Bruce"
+last_name = "Springsteen"
diff --git a/internal/toml-test/tests/valid/table/array-table-array.json b/internal/toml-test/tests/valid/table/array-table-array.json
new file mode 100644
index 0000000..5c3d649
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-table-array.json
@@ -0,0 +1,24 @@
+{
+ "a": [
+ {
+ "b": [
+ {
+ "c": {
+ "d": {
+ "type": "string",
+ "value": "val0"
+ }
+ }
+ },
+ {
+ "c": {
+ "d": {
+ "type": "string",
+ "value": "val1"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/internal/toml-test/tests/valid/table/array-table-array.toml b/internal/toml-test/tests/valid/table/array-table-array.toml
new file mode 100644
index 0000000..a07b0c7
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-table-array.toml
@@ -0,0 +1,7 @@
+[[a]]
+ [[a.b]]
+ [a.b.c]
+ d = "val0"
+ [[a.b]]
+ [a.b.c]
+ d = "val1"
diff --git a/internal/toml-test/tests/valid/table/array-within-dotted.json b/internal/toml-test/tests/valid/table/array-within-dotted.json
new file mode 100644
index 0000000..30b1517
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-within-dotted.json
@@ -0,0 +1,18 @@
+{
+ "fruit": {
+ "apple": {
+ "color": {
+ "type": "string",
+ "value": "red"
+ },
+ "seeds": [
+ {
+ "size": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/array-within-dotted.toml b/internal/toml-test/tests/valid/table/array-within-dotted.toml
new file mode 100644
index 0000000..3ecbeb7
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/array-within-dotted.toml
@@ -0,0 +1,5 @@
+[fruit]
+apple.color = "red"
+
+[[fruit.apple.seeds]]
+size = 2
diff --git a/internal/toml-test/tests/valid/table/empty-name.json b/internal/toml-test/tests/valid/table/empty-name.json
new file mode 100644
index 0000000..fc58e38
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/empty-name.json
@@ -0,0 +1,22 @@
+{
+ "": {
+ "x": {
+ "type": "integer",
+ "value": "1"
+ },
+ "a": {
+ "x": {
+ "type": "integer",
+ "value": "2"
+ }
+ }
+ },
+ "a": {
+ "": {
+ "x": {
+ "type": "integer",
+ "value": "3"
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/empty-name.toml b/internal/toml-test/tests/valid/table/empty-name.toml
new file mode 100644
index 0000000..3c98faa
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/empty-name.toml
@@ -0,0 +1,8 @@
+['']
+x = 1
+
+["".a]
+x = 2
+
+[a.'']
+x = 3
diff --git a/internal/toml-test/tests/valid/table/empty.json b/internal/toml-test/tests/valid/table/empty.json
new file mode 100644
index 0000000..3e82cda
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/empty.json
@@ -0,0 +1,3 @@
+{
+ "a": {}
+}
diff --git a/internal/toml-test/tests/valid/table/empty.toml b/internal/toml-test/tests/valid/table/empty.toml
new file mode 100644
index 0000000..8bb6a0a
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/empty.toml
@@ -0,0 +1 @@
+[a]
diff --git a/internal/toml-test/tests/valid/table/keyword.json b/internal/toml-test/tests/valid/table/keyword.json
new file mode 100644
index 0000000..a15dfae
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/keyword.json
@@ -0,0 +1,6 @@
+{
+ "true": {},
+ "false": {},
+ "inf": {},
+ "nan": {}
+}
diff --git a/internal/toml-test/tests/valid/table/keyword.toml b/internal/toml-test/tests/valid/table/keyword.toml
new file mode 100644
index 0000000..bc3d52a
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/keyword.toml
@@ -0,0 +1,9 @@
+[true]
+
+[false]
+
+[inf]
+
+[nan]
+
+
diff --git a/internal/toml-test/tests/valid/table/names.json b/internal/toml-test/tests/valid/table/names.json
new file mode 100644
index 0000000..66d8ea5
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/names.json
@@ -0,0 +1,30 @@
+{
+ "a": {
+ " x ": {},
+ "b": {
+ "c": {}
+ },
+ "b.c": {},
+ "d.e": {}
+ },
+ "d": {
+ "e": {
+ "f": {}
+ }
+ },
+ "g": {
+ "h": {
+ "i": {}
+ }
+ },
+ "j": {
+ "ʞ": {
+ "l": {}
+ }
+ },
+ "x": {
+ "1": {
+ "2": {}
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/names.toml b/internal/toml-test/tests/valid/table/names.toml
new file mode 100644
index 0000000..092da2f
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/names.toml
@@ -0,0 +1,9 @@
+[a.b.c]
+[a."b.c"]
+[a.'d.e']
+[a.' x ']
+[ d.e.f ]
+[ g . h . i ]
+[ j . "ʞ" . 'l' ]
+
+[x.1.2]
diff --git a/internal/toml-test/tests/valid/table/no-eol.json b/internal/toml-test/tests/valid/table/no-eol.json
new file mode 100644
index 0000000..b6e7fb5
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/no-eol.json
@@ -0,0 +1,3 @@
+{
+ "table": {}
+}
diff --git a/internal/toml-test/tests/valid/table/no-eol.toml b/internal/toml-test/tests/valid/table/no-eol.toml
new file mode 100644
index 0000000..f1098fd
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/no-eol.toml
@@ -0,0 +1 @@
+[table]
diff --git a/internal/toml-test/tests/valid/table/sub-empty.json b/internal/toml-test/tests/valid/table/sub-empty.json
new file mode 100644
index 0000000..ed10cd2
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/sub-empty.json
@@ -0,0 +1,5 @@
+{
+ "a": {
+ "b": {}
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/sub-empty.toml b/internal/toml-test/tests/valid/table/sub-empty.toml
new file mode 100644
index 0000000..70b7fe1
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/sub-empty.toml
@@ -0,0 +1,2 @@
+[a]
+[a.b]
diff --git a/internal/toml-test/tests/valid/table/sub.json b/internal/toml-test/tests/valid/table/sub.json
new file mode 100644
index 0000000..cb81a3f
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/sub.json
@@ -0,0 +1,20 @@
+{
+ "a": {
+ "extend": {
+ "key": {
+ "type": "integer",
+ "value": "2"
+ },
+ "more": {
+ "key": {
+ "type": "integer",
+ "value": "3"
+ }
+ }
+ },
+ "key": {
+ "type": "integer",
+ "value": "1"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/sub.toml b/internal/toml-test/tests/valid/table/sub.toml
new file mode 100644
index 0000000..e8299c2
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/sub.toml
@@ -0,0 +1,9 @@
+[a]
+key = 1
+
+# a.extend is a key inside the "a" table.
+[a.extend]
+key = 2
+
+[a.extend.more]
+key = 3
diff --git a/internal/toml-test/tests/valid/table/whitespace.json b/internal/toml-test/tests/valid/table/whitespace.json
new file mode 100644
index 0000000..c770470
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/whitespace.json
@@ -0,0 +1,3 @@
+{
+ "valid key": {}
+}
diff --git a/internal/toml-test/tests/valid/table/whitespace.toml b/internal/toml-test/tests/valid/table/whitespace.toml
new file mode 100644
index 0000000..daf881d
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/whitespace.toml
@@ -0,0 +1 @@
+["valid key"]
diff --git a/internal/toml-test/tests/valid/table/with-literal-string.json b/internal/toml-test/tests/valid/table/with-literal-string.json
new file mode 100644
index 0000000..7784695
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-literal-string.json
@@ -0,0 +1,12 @@
+{
+ "a": {
+ "\"b\"": {
+ "c": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/with-literal-string.toml b/internal/toml-test/tests/valid/table/with-literal-string.toml
new file mode 100644
index 0000000..b81643d
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-literal-string.toml
@@ -0,0 +1,4 @@
+['a']
+[a.'"b"']
+[a.'"b"'.c]
+answer = 42
diff --git a/internal/toml-test/tests/valid/table/with-pound.json b/internal/toml-test/tests/valid/table/with-pound.json
new file mode 100644
index 0000000..f32d2f2
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-pound.json
@@ -0,0 +1,8 @@
+{
+ "key#group": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/with-pound.toml b/internal/toml-test/tests/valid/table/with-pound.toml
new file mode 100644
index 0000000..33f2c4f
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-pound.toml
@@ -0,0 +1,2 @@
+["key#group"]
+answer = 42
diff --git a/internal/toml-test/tests/valid/table/with-single-quotes.json b/internal/toml-test/tests/valid/table/with-single-quotes.json
new file mode 100644
index 0000000..347c693
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-single-quotes.json
@@ -0,0 +1,12 @@
+{
+ "a": {
+ "b": {
+ "c": {
+ "answer": {
+ "type": "integer",
+ "value": "42"
+ }
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/with-single-quotes.toml b/internal/toml-test/tests/valid/table/with-single-quotes.toml
new file mode 100644
index 0000000..56e52cc
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/with-single-quotes.toml
@@ -0,0 +1,4 @@
+['a']
+[a.'b']
+[a.'b'.c]
+answer = 42
diff --git a/internal/toml-test/tests/valid/table/without-super.json b/internal/toml-test/tests/valid/table/without-super.json
new file mode 100644
index 0000000..3c390c6
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/without-super.json
@@ -0,0 +1,9 @@
+{
+ "x": {
+ "y": {
+ "z": {
+ "w": {}
+ }
+ }
+ }
+}
diff --git a/internal/toml-test/tests/valid/table/without-super.toml b/internal/toml-test/tests/valid/table/without-super.toml
new file mode 100644
index 0000000..73ed68b
--- /dev/null
+++ b/internal/toml-test/tests/valid/table/without-super.toml
@@ -0,0 +1,5 @@
+# [x] you
+# [x.y] don't
+# [x.y.z] need these
+[x.y.z.w] # for this to work
+[x] # defining a super-table afterwards is ok
diff --git a/internal/toml-test/toml.go b/internal/toml-test/toml.go
new file mode 100644
index 0000000..934e631
--- /dev/null
+++ b/internal/toml-test/toml.go
@@ -0,0 +1,136 @@
+//go:build go1.16
+// +build go1.16
+
+package tomltest
+
+import (
+ "math"
+ "reflect"
+)
+
+// CompareTOML compares the given arguments.
+//
+// The returned value is a copy of Test with Failure set to a (human-readable)
+// description of the first element that is unequal. If both arguments are equal
+// Test is returned unchanged.
+//
+// Reflect.DeepEqual could work here, but it won't tell us how the two
+// structures are different.
+func (r Test) CompareTOML(want, have interface{}) Test {
+ if isTomlValue(want) {
+ if !isTomlValue(have) {
+ return r.fail("Type for key '%s' differs:\n"+
+ " Expected: %[2]v (%[2]T)\n"+
+ " Your encoder: %[3]v (%[3]T)",
+ r.Key, want, have)
+ }
+
+ if !deepEqual(want, have) {
+ return r.fail("Values for key '%s' differ:\n"+
+ " Expected: %[2]v (%[2]T)\n"+
+ " Your encoder: %[3]v (%[3]T)",
+ r.Key, want, have)
+ }
+ return r
+ }
+
+ switch w := want.(type) {
+ case map[string]interface{}:
+ return r.cmpTOMLMap(w, have)
+ case []interface{}:
+ return r.cmpTOMLArrays(w, have)
+ default:
+ return r.fail("Unrecognized TOML structure: %T", want)
+ }
+}
+
+func (r Test) cmpTOMLMap(want map[string]interface{}, have interface{}) Test {
+ haveMap, ok := have.(map[string]interface{})
+ if !ok {
+ return r.mismatch("table", want, haveMap)
+ }
+
+ // Check that the keys of each map are equivalent.
+ for k := range want {
+ if _, ok := haveMap[k]; !ok {
+ bunk := r.kjoin(k)
+ return bunk.fail("Could not find key '%s' in encoder output", bunk.Key)
+ }
+ }
+ for k := range haveMap {
+ if _, ok := want[k]; !ok {
+ bunk := r.kjoin(k)
+ return bunk.fail("Could not find key '%s' in expected output", bunk.Key)
+ }
+ }
+
+ // Okay, now make sure that each value is equivalent.
+ for k := range want {
+ if sub := r.kjoin(k).CompareTOML(want[k], haveMap[k]); sub.Failed() {
+ return sub
+ }
+ }
+ return r
+}
+
+func (r Test) cmpTOMLArrays(want []interface{}, have interface{}) Test {
+ // Slice can be decoded to []interface{} for an array of primitives, or
+ // []map[string]interface{} for an array of tables.
+ //
+ // TODO: it would be nicer if it could always decode to []interface{}?
+ haveSlice, ok := have.([]interface{})
+ if !ok {
+ tblArray, ok := have.([]map[string]interface{})
+ if !ok {
+ return r.mismatch("array", want, have)
+ }
+
+ haveSlice = make([]interface{}, len(tblArray))
+ for i := range tblArray {
+ haveSlice[i] = tblArray[i]
+ }
+ }
+
+ if len(want) != len(haveSlice) {
+ return r.fail("Array lengths differ for key '%s'"+
+ " Expected: %[2]v (len=%[4]d)\n"+
+ " Your encoder: %[3]v (len=%[5]d)",
+ r.Key, want, haveSlice, len(want), len(haveSlice))
+ }
+ for i := 0; i < len(want); i++ {
+ if sub := r.CompareTOML(want[i], haveSlice[i]); sub.Failed() {
+ return sub
+ }
+ }
+ return r
+}
+
+// reflect.DeepEqual() that deals with NaN != NaN
+func deepEqual(want, have interface{}) bool {
+ var wantF, haveF float64
+ switch f := want.(type) {
+ case float32:
+ wantF = float64(f)
+ case float64:
+ wantF = f
+ }
+ switch f := have.(type) {
+ case float32:
+ haveF = float64(f)
+ case float64:
+ haveF = f
+ }
+ if math.IsNaN(wantF) && math.IsNaN(haveF) {
+ return true
+ }
+
+ return reflect.DeepEqual(want, have)
+}
+
+func isTomlValue(v interface{}) bool {
+ switch v.(type) {
+ case map[string]interface{}, []interface{}:
+ return false
+ }
+ return true
+}
diff --git a/internal/toml-test/version.go b/internal/toml-test/version.go
new file mode 100644
index 0000000..54454e2
--- /dev/null
+++ b/internal/toml-test/version.go
@@ -0,0 +1,34 @@
+//go:build go1.16
+// +build go1.16
+
+package tomltest
+
+type versionSpec struct {
+ inherit string
+ exclude []string
+}
+
+var versions = map[string]versionSpec{
+ "next": versionSpec{
+ exclude: []string{
+ "invalid/datetime/no-secs", // Times without seconds is no longer invalid.
+ "invalid/string/basic-byte-escapes", // \x is now valid.
+ "invalid/inline-table/trailing-comma",
+ "invalid/inline-table/linebreak-1",
+ "invalid/inline-table/linebreak-2",
+ "invalid/inline-table/linebreak-3",
+ "invalid/inline-table/linebreak-4",
+ "invalid/key/special-character", // Unicode can now be in bare keys.
+ },
+ },
+
+ "1.0.0": versionSpec{
+ exclude: []string{
+ "valid/string/escape-esc", // \e
+ "valid/string/hex-escape", "invalid/string/bad-hex-esc", // \x..
+ "valid/datetime/no-seconds", // Times without seconds
+ "valid/inline-table/newline",
+ "valid/key/unicode", // Unicode bare keys
+ },
+ },
+}
diff --git a/internal/tz.go b/internal/tz.go
new file mode 100644
index 0000000..022f15b
--- /dev/null
+++ b/internal/tz.go
@@ -0,0 +1,36 @@
+package internal
+
+import "time"
+
+// Timezones used for local datetime, date, and time TOML types.
+//
+// The exact way times and dates without a timezone should be interpreted is not
+// well-defined in the TOML specification and left to the implementation. These
+// defaults to current local timezone offset of the computer, but this can be
+// changed by changing these variables before decoding.
+//
+// TODO:
+// Ideally we'd like to offer people the ability to configure the used timezone
+// by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit
+// tricky: the reason we use three different variables for this is to support
+// round-tripping – without these specific TZ names we wouldn't know which
+// format to use.
+//
+// There isn't a good way to encode this right now though, and passing this sort
+// of information also ties in to various related issues such as string format
+// encoding, encoding of comments, etc.
+//
+// So, for the time being, just put this in internal until we can write a good
+// comprehensive API for doing all of this.
+//
+// The reason they're exported is because they're referred from in e.g.
+// internal/tag.
+//
+// Note that this behaviour is valid according to the TOML spec as the exact
+// behaviour is left up to implementations.
+var (
+ localOffset = func() int { _, o := time.Now().Zone(); return o }()
+ LocalDatetime = time.FixedZone("datetime-local", localOffset)
+ LocalDate = time.FixedZone("date-local", localOffset)
+ LocalTime = time.FixedZone("time-local", localOffset)
+)
diff --git a/lex.go b/lex.go
new file mode 100644
index 0000000..3545a6a
--- /dev/null
+++ b/lex.go
@@ -0,0 +1,1283 @@
+package toml
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+type itemType int
+
+const (
+ itemError itemType = iota
+ itemNIL // used in the parser to indicate no type
+ itemEOF
+ itemText
+ itemString
+ itemRawString
+ itemMultilineString
+ itemRawMultilineString
+ itemBool
+ itemInteger
+ itemFloat
+ itemDatetime
+ itemArray // the start of an array
+ itemArrayEnd
+ itemTableStart
+ itemTableEnd
+ itemArrayTableStart
+ itemArrayTableEnd
+ itemKeyStart
+ itemKeyEnd
+ itemCommentStart
+ itemInlineTableStart
+ itemInlineTableEnd
+)
+
+const eof = 0
+
+type stateFn func(lx *lexer) stateFn
+
+func (p Position) String() string {
+ return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len)
+}
+
+type lexer struct {
+ input string
+ start int
+ pos int
+ line int
+ state stateFn
+ items chan item
+ tomlNext bool
+
+ // Allow for backing up up to 4 runes. This is necessary because TOML
+ // contains 3-rune tokens (""" and ''').
+ prevWidths [4]int
+ nprev int // how many of prevWidths are in use
+ atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again.
+
+ // A stack of state functions used to maintain context.
+ //
+ // The idea is to reuse parts of the state machine in various places. For
+ // example, values can appear at the top level or within arbitrarily nested
+ // arrays. The last state on the stack is used after a value has been lexed.
+ // Similarly for comments.
+ stack []stateFn
+}
+
+type item struct {
+ typ itemType
+ val string
+ err error
+ pos Position
+}
+
+func (lx *lexer) nextItem() item {
+ for {
+ select {
+ case item := <-lx.items:
+ return item
+ default:
+ lx.state = lx.state(lx)
+ //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack)
+ }
+ }
+}
+
+func lex(input string, tomlNext bool) *lexer {
+ lx := &lexer{
+ input: input,
+ state: lexTop,
+ items: make(chan item, 10),
+ stack: make([]stateFn, 0, 10),
+ line: 1,
+ tomlNext: tomlNext,
+ }
+ return lx
+}
+
+func (lx *lexer) push(state stateFn) {
+ lx.stack = append(lx.stack, state)
+}
+
+func (lx *lexer) pop() stateFn {
+ if len(lx.stack) == 0 {
+ return lx.errorf("BUG in lexer: no states to pop")
+ }
+ last := lx.stack[len(lx.stack)-1]
+ lx.stack = lx.stack[0 : len(lx.stack)-1]
+ return last
+}
+
+func (lx *lexer) current() string {
+ return lx.input[lx.start:lx.pos]
+}
+
+func (lx lexer) getPos() Position {
+ p := Position{
+ Line: lx.line,
+ Start: lx.start,
+ Len: lx.pos - lx.start,
+ }
+ if p.Len <= 0 {
+ p.Len = 1
+ }
+ return p
+}
+
+func (lx *lexer) emit(typ itemType) {
+ // Needed for multiline strings ending with an incomplete UTF-8 sequence.
+ if lx.start > lx.pos {
+ lx.error(errLexUTF8{lx.input[lx.pos]})
+ return
+ }
+ lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()}
+ lx.start = lx.pos
+}
+
+func (lx *lexer) emitTrim(typ itemType) {
+ lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())}
+ lx.start = lx.pos
+}
+
+func (lx *lexer) next() (r rune) {
+ if lx.atEOF {
+ panic("BUG in lexer: next called after EOF")
+ }
+ if lx.pos >= len(lx.input) {
+ lx.atEOF = true
+ return eof
+ }
+
+ if lx.input[lx.pos] == '\n' {
+ lx.line++
+ }
+ lx.prevWidths[3] = lx.prevWidths[2]
+ lx.prevWidths[2] = lx.prevWidths[1]
+ lx.prevWidths[1] = lx.prevWidths[0]
+ if lx.nprev < 4 {
+ lx.nprev++
+ }
+
+ r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
+ if r == utf8.RuneError {
+ lx.error(errLexUTF8{lx.input[lx.pos]})
+ return utf8.RuneError
+ }
+
+ // Note: don't use peek() here, as this calls next().
+ if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) {
+ lx.errorControlChar(r)
+ return utf8.RuneError
+ }
+
+ lx.prevWidths[0] = w
+ lx.pos += w
+ return r
+}
+
+// ignore skips over the pending input before this point.
+func (lx *lexer) ignore() {
+ lx.start = lx.pos
+}
+
+// backup steps back one rune. Can be called 4 times between calls to next.
+func (lx *lexer) backup() {
+ if lx.atEOF {
+ lx.atEOF = false
+ return
+ }
+ if lx.nprev < 1 {
+ panic("BUG in lexer: backed up too far")
+ }
+ w := lx.prevWidths[0]
+ lx.prevWidths[0] = lx.prevWidths[1]
+ lx.prevWidths[1] = lx.prevWidths[2]
+ lx.prevWidths[2] = lx.prevWidths[3]
+ lx.nprev--
+
+ lx.pos -= w
+ if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
+ lx.line--
+ }
+}
+
+// accept consumes the next rune if it's equal to `valid`.
+func (lx *lexer) accept(valid rune) bool {
+ if lx.next() == valid {
+ return true
+ }
+ lx.backup()
+ return false
+}
+
+// peek returns but does not consume the next rune in the input.
+func (lx *lexer) peek() rune {
+ r := lx.next()
+ lx.backup()
+ return r
+}
+
+// skip ignores all input that matches the given predicate.
+func (lx *lexer) skip(pred func(rune) bool) {
+ for {
+ r := lx.next()
+ if pred(r) {
+ continue
+ }
+ lx.backup()
+ lx.ignore()
+ return
+ }
+}
+
+// error stops all lexing by emitting an error and returning `nil`.
+//
+// Note that any value that is a character is escaped if it's a special
+// character (newlines, tabs, etc.).
+func (lx *lexer) error(err error) stateFn {
+ if lx.atEOF {
+ return lx.errorPrevLine(err)
+ }
+ lx.items <- item{typ: itemError, pos: lx.getPos(), err: err}
+ return nil
+}
+
+// errorfPrevline is like error(), but sets the position to the last column of
+// the previous line.
+//
+// This is so that unexpected EOF or NL errors don't show on a new blank line.
+func (lx *lexer) errorPrevLine(err error) stateFn {
+ pos := lx.getPos()
+ pos.Line--
+ pos.Len = 1
+ pos.Start = lx.pos - 1
+ lx.items <- item{typ: itemError, pos: pos, err: err}
+ return nil
+}
+
+// errorPos is like error(), but allows explicitly setting the position.
+func (lx *lexer) errorPos(start, length int, err error) stateFn {
+ pos := lx.getPos()
+ pos.Start = start
+ pos.Len = length
+ lx.items <- item{typ: itemError, pos: pos, err: err}
+ return nil
+}
+
+// errorf is like error, and creates a new error.
+func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
+ if lx.atEOF {
+ pos := lx.getPos()
+ pos.Line--
+ pos.Len = 1
+ pos.Start = lx.pos - 1
+ lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)}
+ return nil
+ }
+ lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)}
+ return nil
+}
+
+func (lx *lexer) errorControlChar(cc rune) stateFn {
+ return lx.errorPos(lx.pos-1, 1, errLexControl{cc})
+}
+
+// lexTop consumes elements at the top level of TOML data.
+func lexTop(lx *lexer) stateFn {
+ r := lx.next()
+ if isWhitespace(r) || isNL(r) {
+ return lexSkip(lx, lexTop)
+ }
+ switch r {
+ case '#':
+ lx.push(lexTop)
+ return lexCommentStart
+ case '[':
+ return lexTableStart
+ case eof:
+ if lx.pos > lx.start {
+ return lx.errorf("unexpected EOF")
+ }
+ lx.emit(itemEOF)
+ return nil
+ }
+
+ // At this point, the only valid item can be a key, so we back up
+ // and let the key lexer do the rest.
+ lx.backup()
+ lx.push(lexTopEnd)
+ return lexKeyStart
+}
+
+// lexTopEnd is entered whenever a top-level item has been consumed. (A value
+// or a table.) It must see only whitespace, and will turn back to lexTop
+// upon a newline. If it sees EOF, it will quit the lexer successfully.
+func lexTopEnd(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ case r == '#':
+ // a comment will read to a newline for us.
+ lx.push(lexTop)
+ return lexCommentStart
+ case isWhitespace(r):
+ return lexTopEnd
+ case isNL(r):
+ lx.ignore()
+ return lexTop
+ case r == eof:
+ lx.emit(itemEOF)
+ return nil
+ }
+ return lx.errorf(
+ "expected a top-level item to end with a newline, comment, or EOF, but got %q instead",
+ r)
+}
+
+// lexTable lexes the beginning of a table. Namely, it makes sure that
+// it starts with a character other than '.' and ']'.
+// It assumes that '[' has already been consumed.
+// It also handles the case that this is an item in an array of tables.
+// e.g., '[[name]]'.
+func lexTableStart(lx *lexer) stateFn {
+ if lx.peek() == '[' {
+ lx.next()
+ lx.emit(itemArrayTableStart)
+ lx.push(lexArrayTableEnd)
+ } else {
+ lx.emit(itemTableStart)
+ lx.push(lexTableEnd)
+ }
+ return lexTableNameStart
+}
+
+func lexTableEnd(lx *lexer) stateFn {
+ lx.emit(itemTableEnd)
+ return lexTopEnd
+}
+
+func lexArrayTableEnd(lx *lexer) stateFn {
+ if r := lx.next(); r != ']' {
+ return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r)
+ }
+ lx.emit(itemArrayTableEnd)
+ return lexTopEnd
+}
+
+func lexTableNameStart(lx *lexer) stateFn {
+ lx.skip(isWhitespace)
+ switch r := lx.peek(); {
+ case r == ']' || r == eof:
+ return lx.errorf("unexpected end of table name (table names cannot be empty)")
+ case r == '.':
+ return lx.errorf("unexpected table separator (table names cannot be empty)")
+ case r == '"' || r == '\'':
+ lx.ignore()
+ lx.push(lexTableNameEnd)
+ return lexQuotedName
+ default:
+ lx.push(lexTableNameEnd)
+ return lexBareName
+ }
+}
+
+// lexTableNameEnd reads the end of a piece of a table name, optionally
+// consuming whitespace.
+func lexTableNameEnd(lx *lexer) stateFn {
+ lx.skip(isWhitespace)
+ switch r := lx.next(); {
+ case isWhitespace(r):
+ return lexTableNameEnd
+ case r == '.':
+ lx.ignore()
+ return lexTableNameStart
+ case r == ']':
+ return lx.pop()
+ default:
+ return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r)
+ }
+}
+
+// lexBareName lexes one part of a key or table.
+//
+// It assumes that at least one valid character for the table has already been
+// read.
+//
+// Lexes only one part, e.g. only 'a' inside 'a.b'.
+func lexBareName(lx *lexer) stateFn {
+ r := lx.next()
+ if isBareKeyChar(r, lx.tomlNext) {
+ return lexBareName
+ }
+ lx.backup()
+ lx.emit(itemText)
+ return lx.pop()
+}
+
+// lexBareName lexes one part of a key or table.
+//
+// It assumes that at least one valid character for the table has already been
+// read.
+//
+// Lexes only one part, e.g. only '"a"' inside '"a".b'.
+func lexQuotedName(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ case isWhitespace(r):
+ return lexSkip(lx, lexValue)
+ case r == '"':
+ lx.ignore() // ignore the '"'
+ return lexString
+ case r == '\'':
+ lx.ignore() // ignore the "'"
+ return lexRawString
+ case r == eof:
+ return lx.errorf("unexpected EOF; expected value")
+ default:
+ return lx.errorf("expected value but found %q instead", r)
+ }
+}
+
+// lexKeyStart consumes all key parts until a '='.
+func lexKeyStart(lx *lexer) stateFn {
+ lx.skip(isWhitespace)
+ switch r := lx.peek(); {
+ case r == '=' || r == eof:
+ return lx.errorf("unexpected '=': key name appears blank")
+ case r == '.':
+ return lx.errorf("unexpected '.': keys cannot start with a '.'")
+ case r == '"' || r == '\'':
+ lx.ignore()
+ fallthrough
+ default: // Bare key
+ lx.emit(itemKeyStart)
+ return lexKeyNameStart
+ }
+}
+
+func lexKeyNameStart(lx *lexer) stateFn {
+ lx.skip(isWhitespace)
+ switch r := lx.peek(); {
+ case r == '=' || r == eof:
+ return lx.errorf("unexpected '='")
+ case r == '.':
+ return lx.errorf("unexpected '.'")
+ case r == '"' || r == '\'':
+ lx.ignore()
+ lx.push(lexKeyEnd)
+ return lexQuotedName
+ default:
+ lx.push(lexKeyEnd)
+ return lexBareName
+ }
+}
+
+// lexKeyEnd consumes the end of a key and trims whitespace (up to the key
+// separator).
+func lexKeyEnd(lx *lexer) stateFn {
+ lx.skip(isWhitespace)
+ switch r := lx.next(); {
+ case isWhitespace(r):
+ return lexSkip(lx, lexKeyEnd)
+ case r == eof:
+ return lx.errorf("unexpected EOF; expected key separator '='")
+ case r == '.':
+ lx.ignore()
+ return lexKeyNameStart
+ case r == '=':
+ lx.emit(itemKeyEnd)
+ return lexSkip(lx, lexValue)
+ default:
+ return lx.errorf("expected '.' or '=', but got %q instead", r)
+ }
+}
+
+// lexValue starts the consumption of a value anywhere a value is expected.
+// lexValue will ignore whitespace.
+// After a value is lexed, the last state on the next is popped and returned.
+func lexValue(lx *lexer) stateFn {
+ // We allow whitespace to precede a value, but NOT newlines.
+ // In array syntax, the array states are responsible for ignoring newlines.
+ r := lx.next()
+ switch {
+ case isWhitespace(r):
+ return lexSkip(lx, lexValue)
+ case isDigit(r):
+ lx.backup() // avoid an extra state and use the same as above
+ return lexNumberOrDateStart
+ }
+ switch r {
+ case '[':
+ lx.ignore()
+ lx.emit(itemArray)
+ return lexArrayValue
+ case '{':
+ lx.ignore()
+ lx.emit(itemInlineTableStart)
+ return lexInlineTableValue
+ case '"':
+ if lx.accept('"') {
+ if lx.accept('"') {
+ lx.ignore() // Ignore """
+ return lexMultilineString
+ }
+ lx.backup()
+ }
+ lx.ignore() // ignore the '"'
+ return lexString
+ case '\'':
+ if lx.accept('\'') {
+ if lx.accept('\'') {
+ lx.ignore() // Ignore """
+ return lexMultilineRawString
+ }
+ lx.backup()
+ }
+ lx.ignore() // ignore the "'"
+ return lexRawString
+ case '.': // special error case, be kind to users
+ return lx.errorf("floats must start with a digit, not '.'")
+ case 'i', 'n':
+ if (lx.accept('n') && lx.accept('f')) || (lx.accept('a') && lx.accept('n')) {
+ lx.emit(itemFloat)
+ return lx.pop()
+ }
+ case '-', '+':
+ return lexDecimalNumberStart
+ }
+ if unicode.IsLetter(r) {
+ // Be permissive here; lexBool will give a nice error if the
+ // user wrote something like
+ // x = foo
+ // (i.e. not 'true' or 'false' but is something else word-like.)
+ lx.backup()
+ return lexBool
+ }
+ if r == eof {
+ return lx.errorf("unexpected EOF; expected value")
+ }
+ return lx.errorf("expected value but found %q instead", r)
+}
+
+// lexArrayValue consumes one value in an array. It assumes that '[' or ','
+// have already been consumed. All whitespace and newlines are ignored.
+func lexArrayValue(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ case isWhitespace(r) || isNL(r):
+ return lexSkip(lx, lexArrayValue)
+ case r == '#':
+ lx.push(lexArrayValue)
+ return lexCommentStart
+ case r == ',':
+ return lx.errorf("unexpected comma")
+ case r == ']':
+ return lexArrayEnd
+ }
+
+ lx.backup()
+ lx.push(lexArrayValueEnd)
+ return lexValue
+}
+
+// lexArrayValueEnd consumes everything between the end of an array value and
+// the next value (or the end of the array): it ignores whitespace and newlines
+// and expects either a ',' or a ']'.
+func lexArrayValueEnd(lx *lexer) stateFn {
+ switch r := lx.next(); {
+ case isWhitespace(r) || isNL(r):
+ return lexSkip(lx, lexArrayValueEnd)
+ case r == '#':
+ lx.push(lexArrayValueEnd)
+ return lexCommentStart
+ case r == ',':
+ lx.ignore()
+ return lexArrayValue // move on to the next value
+ case r == ']':
+ return lexArrayEnd
+ default:
+ return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r))
+ }
+}
+
+// lexArrayEnd finishes the lexing of an array.
+// It assumes that a ']' has just been consumed.
+func lexArrayEnd(lx *lexer) stateFn {
+ lx.ignore()
+ lx.emit(itemArrayEnd)
+ return lx.pop()
+}
+
+// lexInlineTableValue consumes one key/value pair in an inline table.
+// It assumes that '{' or ',' have already been consumed. Whitespace is ignored.
+func lexInlineTableValue(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ case isWhitespace(r):
+ return lexSkip(lx, lexInlineTableValue)
+ case isNL(r):
+ if lx.tomlNext {
+ return lexSkip(lx, lexInlineTableValue)
+ }
+ return lx.errorPrevLine(errLexInlineTableNL{})
+ case r == '#':
+ lx.push(lexInlineTableValue)
+ return lexCommentStart
+ case r == ',':
+ return lx.errorf("unexpected comma")
+ case r == '}':
+ return lexInlineTableEnd
+ }
+ lx.backup()
+ lx.push(lexInlineTableValueEnd)
+ return lexKeyStart
+}
+
+// lexInlineTableValueEnd consumes everything between the end of an inline table
+// key/value pair and the next pair (or the end of the table):
+// it ignores whitespace and expects either a ',' or a '}'.
+func lexInlineTableValueEnd(lx *lexer) stateFn {
+ switch r := lx.next(); {
+ case isWhitespace(r):
+ return lexSkip(lx, lexInlineTableValueEnd)
+ case isNL(r):
+ if lx.tomlNext {
+ return lexSkip(lx, lexInlineTableValueEnd)
+ }
+ return lx.errorPrevLine(errLexInlineTableNL{})
+ case r == '#':
+ lx.push(lexInlineTableValueEnd)
+ return lexCommentStart
+ case r == ',':
+ lx.ignore()
+ lx.skip(isWhitespace)
+ if lx.peek() == '}' {
+ if lx.tomlNext {
+ return lexInlineTableValueEnd
+ }
+ return lx.errorf("trailing comma not allowed in inline tables")
+ }
+ return lexInlineTableValue
+ case r == '}':
+ return lexInlineTableEnd
+ default:
+ return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r))
+ }
+}
+
+func runeOrEOF(r rune) string {
+ if r == eof {
+ return "end of file"
+ }
+ return "'" + string(r) + "'"
+}
+
+// lexInlineTableEnd finishes the lexing of an inline table.
+// It assumes that a '}' has just been consumed.
+func lexInlineTableEnd(lx *lexer) stateFn {
+ lx.ignore()
+ lx.emit(itemInlineTableEnd)
+ return lx.pop()
+}
+
+// lexString consumes the inner contents of a string. It assumes that the
+// beginning '"' has already been consumed and ignored.
+func lexString(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ case r == eof:
+ return lx.errorf(`unexpected EOF; expected '"'`)
+ case isNL(r):
+ return lx.errorPrevLine(errLexStringNL{})
+ case r == '\\':
+ lx.push(lexString)
+ return lexStringEscape
+ case r == '"':
+ lx.backup()
+ lx.emit(itemString)
+ lx.next()
+ lx.ignore()
+ return lx.pop()
+ }
+ return lexString
+}
+
+// lexMultilineString consumes the inner contents of a string. It assumes that
+// the beginning '"""' has already been consumed and ignored.
+func lexMultilineString(lx *lexer) stateFn {
+ r := lx.next()
+ switch r {
+ default:
+ return lexMultilineString
+ case eof:
+ return lx.errorf(`unexpected EOF; expected '"""'`)
+ case '\\':
+ return lexMultilineStringEscape
+ case '"':
+ /// Found " → try to read two more "".
+ if lx.accept('"') {
+ if lx.accept('"') {
+ /// Peek ahead: the string can contain " and "", including at the
+ /// end: """str"""""
+ /// 6 or more at the end, however, is an error.
+ if lx.peek() == '"' {
+ /// Check if we already lexed 5 's; if so we have 6 now, and
+ /// that's just too many man!
+ ///
+ /// Second check is for the edge case:
+ ///
+ /// two quotes allowed.
+ /// vv
+ /// """lol \""""""
+ /// ^^ ^^^---- closing three
+ /// escaped
+ ///
+ /// But ugly, but it works
+ if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) {
+ return lx.errorf(`unexpected '""""""'`)
+ }
+ lx.backup()
+ lx.backup()
+ return lexMultilineString
+ }
+
+ lx.backup() /// backup: don't include the """ in the item.
+ lx.backup()
+ lx.backup()
+ lx.emit(itemMultilineString)
+ lx.next() /// Read over ''' again and discard it.
+ lx.next()
+ lx.next()
+ lx.ignore()
+ return lx.pop()
+ }
+ lx.backup()
+ }
+ return lexMultilineString
+ }
+}
+
+// lexRawString consumes a raw string. Nothing can be escaped in such a string.
+// It assumes that the beginning "'" has already been consumed and ignored.
+func lexRawString(lx *lexer) stateFn {
+ r := lx.next()
+ switch {
+ default:
+ return lexRawString
+ case r == eof:
+ return lx.errorf(`unexpected EOF; expected "'"`)
+ case isNL(r):
+ return lx.errorPrevLine(errLexStringNL{})
+ case r == '\'':
+ lx.backup()
+ lx.emit(itemRawString)
+ lx.next()
+ lx.ignore()
+ return lx.pop()
+ }
+}
+
+// lexMultilineRawString consumes a raw string. Nothing can be escaped in such a
+// string. It assumes that the beginning triple-' has already been consumed and
+// ignored.
+func lexMultilineRawString(lx *lexer) stateFn {
+ r := lx.next()
+ switch r {
+ default:
+ return lexMultilineRawString
+ case eof:
+ return lx.errorf(`unexpected EOF; expected "'''"`)
+ case '\'':
+ /// Found ' → try to read two more ''.
+ if lx.accept('\'') {
+ if lx.accept('\'') {
+ /// Peek ahead: the string can contain ' and '', including at the
+ /// end: '''str'''''
+ /// 6 or more at the end, however, is an error.
+ if lx.peek() == '\'' {
+ /// Check if we already lexed 5 's; if so we have 6 now, and
+ /// that's just too many man!
+ if strings.HasSuffix(lx.current(), "'''''") {
+ return lx.errorf(`unexpected "''''''"`)
+ }
+ lx.backup()
+ lx.backup()
+ return lexMultilineRawString
+ }
+
+ lx.backup() /// backup: don't include the ''' in the item.
+ lx.backup()
+ lx.backup()
+ lx.emit(itemRawMultilineString)
+ lx.next() /// Read over ''' again and discard it.
+ lx.next()
+ lx.next()
+ lx.ignore()
+ return lx.pop()
+ }
+ lx.backup()
+ }
+ return lexMultilineRawString
+ }
+}
+
+// lexMultilineStringEscape consumes an escaped character. It assumes that the
+// preceding '\\' has already been consumed.
+func lexMultilineStringEscape(lx *lexer) stateFn {
+ if isNL(lx.next()) { /// \ escaping newline.
+ return lexMultilineString
+ }
+ lx.backup()
+ lx.push(lexMultilineString)
+ return lexStringEscape(lx)
+}
+
+func lexStringEscape(lx *lexer) stateFn {
+ r := lx.next()
+ switch r {
+ case 'e':
+ if !lx.tomlNext {
+ return lx.error(errLexEscape{r})
+ }
+ fallthrough
+ case 'b':
+ fallthrough
+ case 't':
+ fallthrough
+ case 'n':
+ fallthrough
+ case 'f':
+ fallthrough
+ case 'r':
+ fallthrough
+ case '"':
+ fallthrough
+ case ' ', '\t':
+ // Inside """ .. """ strings you can use \ to escape newlines, and any
+ // amount of whitespace can be between the \ and \n.
+ fallthrough
+ case '\\':
+ return lx.pop()
+ case 'x':
+ if !lx.tomlNext {
+ return lx.error(errLexEscape{r})
+ }
+ return lexHexEscape
+ case 'u':
+ return lexShortUnicodeEscape
+ case 'U':
+ return lexLongUnicodeEscape
+ }
+ return lx.error(errLexEscape{r})
+}
+
+func lexHexEscape(lx *lexer) stateFn {
+ var r rune
+ for i := 0; i < 2; i++ {
+ r = lx.next()
+ if !isHexadecimal(r) {
+ return lx.errorf(
+ `expected two hexadecimal digits after '\x', but got %q instead`,
+ lx.current())
+ }
+ }
+ return lx.pop()
+}
+
+func lexShortUnicodeEscape(lx *lexer) stateFn {
+ var r rune
+ for i := 0; i < 4; i++ {
+ r = lx.next()
+ if !isHexadecimal(r) {
+ return lx.errorf(
+ `expected four hexadecimal digits after '\u', but got %q instead`,
+ lx.current())
+ }
+ }
+ return lx.pop()
+}
+
+func lexLongUnicodeEscape(lx *lexer) stateFn {
+ var r rune
+ for i := 0; i < 8; i++ {
+ r = lx.next()
+ if !isHexadecimal(r) {
+ return lx.errorf(
+ `expected eight hexadecimal digits after '\U', but got %q instead`,
+ lx.current())
+ }
+ }
+ return lx.pop()
+}
+
+// lexNumberOrDateStart processes the first character of a value which begins
+// with a digit. It exists to catch values starting with '0', so that
+// lexBaseNumberOrDate can differentiate base prefixed integers from other
+// types.
+func lexNumberOrDateStart(lx *lexer) stateFn {
+ r := lx.next()
+ switch r {
+ case '0':
+ return lexBaseNumberOrDate
+ }
+
+ if !isDigit(r) {
+ // The only way to reach this state is if the value starts
+ // with a digit, so specifically treat anything else as an
+ // error.
+ return lx.errorf("expected a digit but got %q", r)
+ }
+
+ return lexNumberOrDate
+}
+
+// lexNumberOrDate consumes either an integer, float or datetime.
+func lexNumberOrDate(lx *lexer) stateFn {
+ r := lx.next()
+ if isDigit(r) {
+ return lexNumberOrDate
+ }
+ switch r {
+ case '-', ':':
+ return lexDatetime
+ case '_':
+ return lexDecimalNumber
+ case '.', 'e', 'E':
+ return lexFloat
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexDatetime consumes a Datetime, to a first approximation.
+// The parser validates that it matches one of the accepted formats.
+func lexDatetime(lx *lexer) stateFn {
+ r := lx.next()
+ if isDigit(r) {
+ return lexDatetime
+ }
+ switch r {
+ case '-', ':', 'T', 't', ' ', '.', 'Z', 'z', '+':
+ return lexDatetime
+ }
+
+ lx.backup()
+ lx.emitTrim(itemDatetime)
+ return lx.pop()
+}
+
+// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix.
+func lexHexInteger(lx *lexer) stateFn {
+ r := lx.next()
+ if isHexadecimal(r) {
+ return lexHexInteger
+ }
+ switch r {
+ case '_':
+ return lexHexInteger
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexOctalInteger consumes an octal integer after seeing the '0o' prefix.
+func lexOctalInteger(lx *lexer) stateFn {
+ r := lx.next()
+ if isOctal(r) {
+ return lexOctalInteger
+ }
+ switch r {
+ case '_':
+ return lexOctalInteger
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexBinaryInteger consumes a binary integer after seeing the '0b' prefix.
+func lexBinaryInteger(lx *lexer) stateFn {
+ r := lx.next()
+ if isBinary(r) {
+ return lexBinaryInteger
+ }
+ switch r {
+ case '_':
+ return lexBinaryInteger
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexDecimalNumber consumes a decimal float or integer.
+func lexDecimalNumber(lx *lexer) stateFn {
+ r := lx.next()
+ if isDigit(r) {
+ return lexDecimalNumber
+ }
+ switch r {
+ case '.', 'e', 'E':
+ return lexFloat
+ case '_':
+ return lexDecimalNumber
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexDecimalNumber consumes the first digit of a number beginning with a sign.
+// It assumes the sign has already been consumed. Values which start with a sign
+// are only allowed to be decimal integers or floats.
+//
+// The special "nan" and "inf" values are also recognized.
+func lexDecimalNumberStart(lx *lexer) stateFn {
+ r := lx.next()
+
+ // Special error cases to give users better error messages
+ switch r {
+ case 'i':
+ if !lx.accept('n') || !lx.accept('f') {
+ return lx.errorf("invalid float: '%s'", lx.current())
+ }
+ lx.emit(itemFloat)
+ return lx.pop()
+ case 'n':
+ if !lx.accept('a') || !lx.accept('n') {
+ return lx.errorf("invalid float: '%s'", lx.current())
+ }
+ lx.emit(itemFloat)
+ return lx.pop()
+ case '0':
+ p := lx.peek()
+ switch p {
+ case 'b', 'o', 'x':
+ return lx.errorf("cannot use sign with non-decimal numbers: '%s%c'", lx.current(), p)
+ }
+ case '.':
+ return lx.errorf("floats must start with a digit, not '.'")
+ }
+
+ if isDigit(r) {
+ return lexDecimalNumber
+ }
+
+ return lx.errorf("expected a digit but got %q", r)
+}
+
+// lexBaseNumberOrDate differentiates between the possible values which
+// start with '0'. It assumes that before reaching this state, the initial '0'
+// has been consumed.
+func lexBaseNumberOrDate(lx *lexer) stateFn {
+ r := lx.next()
+ // Note: All datetimes start with at least two digits, so we don't
+ // handle date characters (':', '-', etc.) here.
+ if isDigit(r) {
+ return lexNumberOrDate
+ }
+ switch r {
+ case '_':
+ // Can only be decimal, because there can't be an underscore
+ // between the '0' and the base designator, and dates can't
+ // contain underscores.
+ return lexDecimalNumber
+ case '.', 'e', 'E':
+ return lexFloat
+ case 'b':
+ r = lx.peek()
+ if !isBinary(r) {
+ lx.errorf("not a binary number: '%s%c'", lx.current(), r)
+ }
+ return lexBinaryInteger
+ case 'o':
+ r = lx.peek()
+ if !isOctal(r) {
+ lx.errorf("not an octal number: '%s%c'", lx.current(), r)
+ }
+ return lexOctalInteger
+ case 'x':
+ r = lx.peek()
+ if !isHexadecimal(r) {
+ lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r)
+ }
+ return lexHexInteger
+ }
+
+ lx.backup()
+ lx.emit(itemInteger)
+ return lx.pop()
+}
+
+// lexFloat consumes the elements of a float. It allows any sequence of
+// float-like characters, so floats emitted by the lexer are only a first
+// approximation and must be validated by the parser.
+func lexFloat(lx *lexer) stateFn {
+ r := lx.next()
+ if isDigit(r) {
+ return lexFloat
+ }
+ switch r {
+ case '_', '.', '-', '+', 'e', 'E':
+ return lexFloat
+ }
+
+ lx.backup()
+ lx.emit(itemFloat)
+ return lx.pop()
+}
+
+// lexBool consumes a bool string: 'true' or 'false.
+func lexBool(lx *lexer) stateFn {
+ var rs []rune
+ for {
+ r := lx.next()
+ if !unicode.IsLetter(r) {
+ lx.backup()
+ break
+ }
+ rs = append(rs, r)
+ }
+ s := string(rs)
+ switch s {
+ case "true", "false":
+ lx.emit(itemBool)
+ return lx.pop()
+ }
+ return lx.errorf("expected value but found %q instead", s)
+}
+
+// lexCommentStart begins the lexing of a comment. It will emit
+// itemCommentStart and consume no characters, passing control to lexComment.
+func lexCommentStart(lx *lexer) stateFn {
+ lx.ignore()
+ lx.emit(itemCommentStart)
+ return lexComment
+}
+
+// lexComment lexes an entire comment. It assumes that '#' has been consumed.
+// It will consume *up to* the first newline character, and pass control
+// back to the last state on the stack.
+func lexComment(lx *lexer) stateFn {
+ switch r := lx.next(); {
+ case isNL(r) || r == eof:
+ lx.backup()
+ lx.emit(itemText)
+ return lx.pop()
+ default:
+ return lexComment
+ }
+}
+
+// lexSkip ignores all slurped input and moves on to the next state.
+func lexSkip(lx *lexer, nextState stateFn) stateFn {
+ lx.ignore()
+ return nextState
+}
+
+func (s stateFn) String() string {
+ name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
+ if i := strings.LastIndexByte(name, '.'); i > -1 {
+ name = name[i+1:]
+ }
+ if s == nil {
+ name = "<nil>"
+ }
+ return name + "()"
+}
+
+func (itype itemType) String() string {
+ switch itype {
+ case itemError:
+ return "Error"
+ case itemNIL:
+ return "NIL"
+ case itemEOF:
+ return "EOF"
+ case itemText:
+ return "Text"
+ case itemString, itemRawString, itemMultilineString, itemRawMultilineString:
+ return "String"
+ case itemBool:
+ return "Bool"
+ case itemInteger:
+ return "Integer"
+ case itemFloat:
+ return "Float"
+ case itemDatetime:
+ return "DateTime"
+ case itemTableStart:
+ return "TableStart"
+ case itemTableEnd:
+ return "TableEnd"
+ case itemKeyStart:
+ return "KeyStart"
+ case itemKeyEnd:
+ return "KeyEnd"
+ case itemArray:
+ return "Array"
+ case itemArrayEnd:
+ return "ArrayEnd"
+ case itemCommentStart:
+ return "CommentStart"
+ case itemInlineTableStart:
+ return "InlineTableStart"
+ case itemInlineTableEnd:
+ return "InlineTableEnd"
+ }
+ panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
+}
+
+func (item item) String() string {
+ return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
+}
+
+func isWhitespace(r rune) bool { return r == '\t' || r == ' ' }
+func isNL(r rune) bool { return r == '\n' || r == '\r' }
+func isControl(r rune) bool { // Control characters except \t, \r, \n
+ switch r {
+ case '\t', '\r', '\n':
+ return false
+ default:
+ return (r >= 0x00 && r <= 0x1f) || r == 0x7f
+ }
+}
+func isDigit(r rune) bool { return r >= '0' && r <= '9' }
+func isBinary(r rune) bool { return r == '0' || r == '1' }
+func isOctal(r rune) bool { return r >= '0' && r <= '7' }
+func isHexadecimal(r rune) bool {
+ return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
+}
+
+func isBareKeyChar(r rune, tomlNext bool) bool {
+ if tomlNext {
+ return (r >= 'A' && r <= 'Z') ||
+ (r >= 'a' && r <= 'z') ||
+ (r >= '0' && r <= '9') ||
+ r == '_' || r == '-' ||
+ r == 0xb2 || r == 0xb3 || r == 0xb9 || (r >= 0xbc && r <= 0xbe) ||
+ (r >= 0xc0 && r <= 0xd6) || (r >= 0xd8 && r <= 0xf6) || (r >= 0xf8 && r <= 0x037d) ||
+ (r >= 0x037f && r <= 0x1fff) ||
+ (r >= 0x200c && r <= 0x200d) || (r >= 0x203f && r <= 0x2040) ||
+ (r >= 0x2070 && r <= 0x218f) || (r >= 0x2460 && r <= 0x24ff) ||
+ (r >= 0x2c00 && r <= 0x2fef) || (r >= 0x3001 && r <= 0xd7ff) ||
+ (r >= 0xf900 && r <= 0xfdcf) || (r >= 0xfdf0 && r <= 0xfffd) ||
+ (r >= 0x10000 && r <= 0xeffff)
+ }
+
+ return (r >= 'A' && r <= 'Z') ||
+ (r >= 'a' && r <= 'z') ||
+ (r >= '0' && r <= '9') ||
+ r == '_' || r == '-'
+}
diff --git a/meta.go b/meta.go
new file mode 100644
index 0000000..2e78b24
--- /dev/null
+++ b/meta.go
@@ -0,0 +1,121 @@
+package toml
+
+import (
+ "strings"
+)
+
+// MetaData allows access to meta information about TOML data that's not
+// accessible otherwise.
+//
+// It allows checking if a key is defined in the TOML data, whether any keys
+// were undecoded, and the TOML type of a key.
+type MetaData struct {
+ context Key // Used only during decoding.
+
+ keyInfo map[string]keyInfo
+ mapping map[string]interface{}
+ keys []Key
+ decoded map[string]struct{}
+ data []byte // Input file; for errors.
+}
+
+// IsDefined reports if the key exists in the TOML data.
+//
+// The key should be specified hierarchically, for example to access the TOML
+// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive.
+//
+// Returns false for an empty key.
+func (md *MetaData) IsDefined(key ...string) bool {
+ if len(key) == 0 {
+ return false
+ }
+
+ var (
+ hash map[string]interface{}
+ ok bool
+ hashOrVal interface{} = md.mapping
+ )
+ for _, k := range key {
+ if hash, ok = hashOrVal.(map[string]interface{}); !ok {
+ return false
+ }
+ if hashOrVal, ok = hash[k]; !ok {
+ return false
+ }
+ }
+ return true
+}
+
+// Type returns a string representation of the type of the key specified.
+//
+// Type will return the empty string if given an empty key or a key that does
+// not exist. Keys are case sensitive.
+func (md *MetaData) Type(key ...string) string {
+ if ki, ok := md.keyInfo[Key(key).String()]; ok {
+ return ki.tomlType.typeString()
+ }
+ return ""
+}
+
+// Keys returns a slice of every key in the TOML data, including key groups.
+//
+// Each key is itself a slice, where the first element is the top of the
+// hierarchy and the last is the most specific. The list will have the same
+// order as the keys appeared in the TOML data.
+//
+// All keys returned are non-empty.
+func (md *MetaData) Keys() []Key {
+ return md.keys
+}
+
+// Undecoded returns all keys that have not been decoded in the order in which
+// they appear in the original TOML document.
+//
+// This includes keys that haven't been decoded because of a [Primitive] value.
+// Once the Primitive value is decoded, the keys will be considered decoded.
+//
+// Also note that decoding into an empty interface will result in no decoding,
+// and so no keys will be considered decoded.
+//
+// In this sense, the Undecoded keys correspond to keys in the TOML document
+// that do not have a concrete type in your representation.
+func (md *MetaData) Undecoded() []Key {
+ undecoded := make([]Key, 0, len(md.keys))
+ for _, key := range md.keys {
+ if _, ok := md.decoded[key.String()]; !ok {
+ undecoded = append(undecoded, key)
+ }
+ }
+ return undecoded
+}
+
+// Key represents any TOML key, including key groups. Use [MetaData.Keys] to get
+// values of this type.
+type Key []string
+
+func (k Key) String() string {
+ ss := make([]string, len(k))
+ for i := range k {
+ ss[i] = k.maybeQuoted(i)
+ }
+ return strings.Join(ss, ".")
+}
+
+func (k Key) maybeQuoted(i int) string {
+ if k[i] == "" {
+ return `""`
+ }
+ for _, c := range k[i] {
+ if !isBareKeyChar(c, false) {
+ return `"` + dblQuotedReplacer.Replace(k[i]) + `"`
+ }
+ }
+ return k[i]
+}
+
+func (k Key) add(piece string) Key {
+ newKey := make(Key, len(k)+1)
+ copy(newKey, k)
+ newKey[len(k)] = piece
+ return newKey
+}
diff --git a/parse.go b/parse.go
new file mode 100644
index 0000000..9c19153
--- /dev/null
+++ b/parse.go
@@ -0,0 +1,811 @@
+package toml
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+type parser struct {
+ lx *lexer
+ context Key // Full key for the current hash in scope.
+ currentKey string // Base key name for everything except hashes.
+ pos Position // Current position in the TOML file.
+ tomlNext bool
+
+ ordered []Key // List of keys in the order that they appear in the TOML data.
+
+ keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
+ mapping map[string]interface{} // Map keyname → key value.
+ implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
+}
+
+type keyInfo struct {
+ pos Position
+ tomlType tomlType
+}
+
+func parse(data string) (p *parser, err error) {
+ _, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
+
+ defer func() {
+ if r := recover(); r != nil {
+ if pErr, ok := r.(ParseError); ok {
+ pErr.input = data
+ err = pErr
+ return
+ }
+ panic(r)
+ }
+ }()
+
+ // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
+ // which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add
+ // it anyway.
+ if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16
+ data = data[2:]
+ } else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8
+ data = data[3:]
+ }
+
+ // Examine first few bytes for NULL bytes; this probably means it's a UTF-16
+ // file (second byte in surrogate pair being NULL). Again, do this here to
+ // avoid having to deal with UTF-8/16 stuff in the lexer.
+ ex := 6
+ if len(data) < 6 {
+ ex = len(data)
+ }
+ if i := strings.IndexRune(data[:ex], 0); i > -1 {
+ return nil, ParseError{
+ Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
+ Position: Position{Line: 1, Start: i, Len: 1},
+ Line: 1,
+ input: data,
+ }
+ }
+
+ p = &parser{
+ keyInfo: make(map[string]keyInfo),
+ mapping: make(map[string]interface{}),
+ lx: lex(data, tomlNext),
+ ordered: make([]Key, 0),
+ implicits: make(map[string]struct{}),
+ tomlNext: tomlNext,
+ }
+ for {
+ item := p.next()
+ if item.typ == itemEOF {
+ break
+ }
+ p.topLevel(item)
+ }
+
+ return p, nil
+}
+
+func (p *parser) panicErr(it item, err error) {
+ panic(ParseError{
+ err: err,
+ Position: it.pos,
+ Line: it.pos.Len,
+ LastKey: p.current(),
+ })
+}
+
+func (p *parser) panicItemf(it item, format string, v ...interface{}) {
+ panic(ParseError{
+ Message: fmt.Sprintf(format, v...),
+ Position: it.pos,
+ Line: it.pos.Len,
+ LastKey: p.current(),
+ })
+}
+
+func (p *parser) panicf(format string, v ...interface{}) {
+ panic(ParseError{
+ Message: fmt.Sprintf(format, v...),
+ Position: p.pos,
+ Line: p.pos.Line,
+ LastKey: p.current(),
+ })
+}
+
+func (p *parser) next() item {
+ it := p.lx.nextItem()
+ //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val)
+ if it.typ == itemError {
+ if it.err != nil {
+ panic(ParseError{
+ Position: it.pos,
+ Line: it.pos.Line,
+ LastKey: p.current(),
+ err: it.err,
+ })
+ }
+
+ p.panicItemf(it, "%s", it.val)
+ }
+ return it
+}
+
+func (p *parser) nextPos() item {
+ it := p.next()
+ p.pos = it.pos
+ return it
+}
+
+func (p *parser) bug(format string, v ...interface{}) {
+ panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
+}
+
+func (p *parser) expect(typ itemType) item {
+ it := p.next()
+ p.assertEqual(typ, it.typ)
+ return it
+}
+
+func (p *parser) assertEqual(expected, got itemType) {
+ if expected != got {
+ p.bug("Expected '%s' but got '%s'.", expected, got)
+ }
+}
+
+func (p *parser) topLevel(item item) {
+ switch item.typ {
+ case itemCommentStart: // # ..
+ p.expect(itemText)
+ case itemTableStart: // [ .. ]
+ name := p.nextPos()
+
+ var key Key
+ for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
+ key = append(key, p.keyString(name))
+ }
+ p.assertEqual(itemTableEnd, name.typ)
+
+ p.addContext(key, false)
+ p.setType("", tomlHash, item.pos)
+ p.ordered = append(p.ordered, key)
+ case itemArrayTableStart: // [[ .. ]]
+ name := p.nextPos()
+
+ var key Key
+ for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
+ key = append(key, p.keyString(name))
+ }
+ p.assertEqual(itemArrayTableEnd, name.typ)
+
+ p.addContext(key, true)
+ p.setType("", tomlArrayHash, item.pos)
+ p.ordered = append(p.ordered, key)
+ case itemKeyStart: // key = ..
+ outerContext := p.context
+ /// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
+ k := p.nextPos()
+ var key Key
+ for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
+ key = append(key, p.keyString(k))
+ }
+ p.assertEqual(itemKeyEnd, k.typ)
+
+ /// The current key is the last part.
+ p.currentKey = key[len(key)-1]
+
+ /// All the other parts (if any) are the context; need to set each part
+ /// as implicit.
+ context := key[:len(key)-1]
+ for i := range context {
+ p.addImplicitContext(append(p.context, context[i:i+1]...))
+ }
+ p.ordered = append(p.ordered, p.context.add(p.currentKey))
+
+ /// Set value.
+ vItem := p.next()
+ val, typ := p.value(vItem, false)
+ p.set(p.currentKey, val, typ, vItem.pos)
+
+ /// Remove the context we added (preserving any context from [tbl] lines).
+ p.context = outerContext
+ p.currentKey = ""
+ default:
+ p.bug("Unexpected type at top level: %s", item.typ)
+ }
+}
+
+// Gets a string for a key (or part of a key in a table name).
+func (p *parser) keyString(it item) string {
+ switch it.typ {
+ case itemText:
+ return it.val
+ case itemString, itemMultilineString,
+ itemRawString, itemRawMultilineString:
+ s, _ := p.value(it, false)
+ return s.(string)
+ default:
+ p.bug("Unexpected key type: %s", it.typ)
+ }
+ panic("unreachable")
+}
+
+var datetimeRepl = strings.NewReplacer(
+ "z", "Z",
+ "t", "T",
+ " ", "T")
+
+// value translates an expected value from the lexer into a Go value wrapped
+// as an empty interface.
+func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
+ switch it.typ {
+ case itemString:
+ return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it)
+ case itemMultilineString:
+ return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it)
+ case itemRawString:
+ return it.val, p.typeOfPrimitive(it)
+ case itemRawMultilineString:
+ return stripFirstNewline(it.val), p.typeOfPrimitive(it)
+ case itemInteger:
+ return p.valueInteger(it)
+ case itemFloat:
+ return p.valueFloat(it)
+ case itemBool:
+ switch it.val {
+ case "true":
+ return true, p.typeOfPrimitive(it)
+ case "false":
+ return false, p.typeOfPrimitive(it)
+ default:
+ p.bug("Expected boolean value, but got '%s'.", it.val)
+ }
+ case itemDatetime:
+ return p.valueDatetime(it)
+ case itemArray:
+ return p.valueArray(it)
+ case itemInlineTableStart:
+ return p.valueInlineTable(it, parentIsArray)
+ default:
+ p.bug("Unexpected value type: %s", it.typ)
+ }
+ panic("unreachable")
+}
+
+func (p *parser) valueInteger(it item) (interface{}, tomlType) {
+ if !numUnderscoresOK(it.val) {
+ p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val)
+ }
+ if numHasLeadingZero(it.val) {
+ p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val)
+ }
+
+ num, err := strconv.ParseInt(it.val, 0, 64)
+ if err != nil {
+ // Distinguish integer values. Normally, it'd be a bug if the lexer
+ // provides an invalid integer, but it's possible that the number is
+ // out of range of valid values (which the lexer cannot determine).
+ // So mark the former as a bug but the latter as a legitimate user
+ // error.
+ if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
+ p.panicErr(it, errParseRange{i: it.val, size: "int64"})
+ } else {
+ p.bug("Expected integer value, but got '%s'.", it.val)
+ }
+ }
+ return num, p.typeOfPrimitive(it)
+}
+
+func (p *parser) valueFloat(it item) (interface{}, tomlType) {
+ parts := strings.FieldsFunc(it.val, func(r rune) bool {
+ switch r {
+ case '.', 'e', 'E':
+ return true
+ }
+ return false
+ })
+ for _, part := range parts {
+ if !numUnderscoresOK(part) {
+ p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val)
+ }
+ }
+ if len(parts) > 0 && numHasLeadingZero(parts[0]) {
+ p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val)
+ }
+ if !numPeriodsOK(it.val) {
+ // As a special case, numbers like '123.' or '1.e2',
+ // which are valid as far as Go/strconv are concerned,
+ // must be rejected because TOML says that a fractional
+ // part consists of '.' followed by 1+ digits.
+ p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val)
+ }
+ val := strings.Replace(it.val, "_", "", -1)
+ if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does.
+ val = "nan"
+ }
+ num, err := strconv.ParseFloat(val, 64)
+ if err != nil {
+ if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
+ p.panicErr(it, errParseRange{i: it.val, size: "float64"})
+ } else {
+ p.panicItemf(it, "Invalid float value: %q", it.val)
+ }
+ }
+ return num, p.typeOfPrimitive(it)
+}
+
+var dtTypes = []struct {
+ fmt string
+ zone *time.Location
+ next bool
+}{
+ {time.RFC3339Nano, time.Local, false},
+ {"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
+ {"2006-01-02", internal.LocalDate, false},
+ {"15:04:05.999999999", internal.LocalTime, false},
+
+ // tomlNext
+ {"2006-01-02T15:04Z07:00", time.Local, true},
+ {"2006-01-02T15:04", internal.LocalDatetime, true},
+ {"15:04", internal.LocalTime, true},
+}
+
+func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
+ it.val = datetimeRepl.Replace(it.val)
+ var (
+ t time.Time
+ ok bool
+ err error
+ )
+ for _, dt := range dtTypes {
+ if dt.next && !p.tomlNext {
+ continue
+ }
+ t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
+ if err == nil {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val)
+ }
+ return t, p.typeOfPrimitive(it)
+}
+
+func (p *parser) valueArray(it item) (interface{}, tomlType) {
+ p.setType(p.currentKey, tomlArray, it.pos)
+
+ var (
+ types []tomlType
+
+ // Initialize to a non-nil empty slice. This makes it consistent with
+ // how S = [] decodes into a non-nil slice inside something like struct
+ // { S []string }. See #338
+ array = []interface{}{}
+ )
+ for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
+ if it.typ == itemCommentStart {
+ p.expect(itemText)
+ continue
+ }
+
+ val, typ := p.value(it, true)
+ array = append(array, val)
+ types = append(types, typ)
+
+ // XXX: types isn't used here, we need it to record the accurate type
+ // information.
+ //
+ // Not entirely sure how to best store this; could use "key[0]",
+ // "key[1]" notation, or maybe store it on the Array type?
+ _ = types
+ }
+ return array, tomlArray
+}
+
+func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) {
+ var (
+ hash = make(map[string]interface{})
+ outerContext = p.context
+ outerKey = p.currentKey
+ )
+
+ p.context = append(p.context, p.currentKey)
+ prevContext := p.context
+ p.currentKey = ""
+
+ p.addImplicit(p.context)
+ p.addContext(p.context, parentIsArray)
+
+ /// Loop over all table key/value pairs.
+ for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
+ if it.typ == itemCommentStart {
+ p.expect(itemText)
+ continue
+ }
+
+ /// Read all key parts.
+ k := p.nextPos()
+ var key Key
+ for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
+ key = append(key, p.keyString(k))
+ }
+ p.assertEqual(itemKeyEnd, k.typ)
+
+ /// The current key is the last part.
+ p.currentKey = key[len(key)-1]
+
+ /// All the other parts (if any) are the context; need to set each part
+ /// as implicit.
+ context := key[:len(key)-1]
+ for i := range context {
+ p.addImplicitContext(append(p.context, context[i:i+1]...))
+ }
+ p.ordered = append(p.ordered, p.context.add(p.currentKey))
+
+ /// Set the value.
+ val, typ := p.value(p.next(), false)
+ p.set(p.currentKey, val, typ, it.pos)
+ hash[p.currentKey] = val
+
+ /// Restore context.
+ p.context = prevContext
+ }
+ p.context = outerContext
+ p.currentKey = outerKey
+ return hash, tomlHash
+}
+
+// numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
+// +/- signs, and base prefixes.
+func numHasLeadingZero(s string) bool {
+ if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x
+ return true
+ }
+ if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' {
+ return true
+ }
+ return false
+}
+
+// numUnderscoresOK checks whether each underscore in s is surrounded by
+// characters that are not underscores.
+func numUnderscoresOK(s string) bool {
+ switch s {
+ case "nan", "+nan", "-nan", "inf", "-inf", "+inf":
+ return true
+ }
+ accept := false
+ for _, r := range s {
+ if r == '_' {
+ if !accept {
+ return false
+ }
+ }
+
+ // isHexadecimal is a superset of all the permissable characters
+ // surrounding an underscore.
+ accept = isHexadecimal(r)
+ }
+ return accept
+}
+
+// numPeriodsOK checks whether every period in s is followed by a digit.
+func numPeriodsOK(s string) bool {
+ period := false
+ for _, r := range s {
+ if period && !isDigit(r) {
+ return false
+ }
+ period = r == '.'
+ }
+ return !period
+}
+
+// Set the current context of the parser, where the context is either a hash or
+// an array of hashes, depending on the value of the `array` parameter.
+//
+// Establishing the context also makes sure that the key isn't a duplicate, and
+// will create implicit hashes automatically.
+func (p *parser) addContext(key Key, array bool) {
+ var ok bool
+
+ // Always start at the top level and drill down for our context.
+ hashContext := p.mapping
+ keyContext := make(Key, 0)
+
+ // We only need implicit hashes for key[0:-1]
+ for _, k := range key[0 : len(key)-1] {
+ _, ok = hashContext[k]
+ keyContext = append(keyContext, k)
+
+ // No key? Make an implicit hash and move on.
+ if !ok {
+ p.addImplicit(keyContext)
+ hashContext[k] = make(map[string]interface{})
+ }
+
+ // If the hash context is actually an array of tables, then set
+ // the hash context to the last element in that array.
+ //
+ // Otherwise, it better be a table, since this MUST be a key group (by
+ // virtue of it not being the last element in a key).
+ switch t := hashContext[k].(type) {
+ case []map[string]interface{}:
+ hashContext = t[len(t)-1]
+ case map[string]interface{}:
+ hashContext = t
+ default:
+ p.panicf("Key '%s' was already created as a hash.", keyContext)
+ }
+ }
+
+ p.context = keyContext
+ if array {
+ // If this is the first element for this array, then allocate a new
+ // list of tables for it.
+ k := key[len(key)-1]
+ if _, ok := hashContext[k]; !ok {
+ hashContext[k] = make([]map[string]interface{}, 0, 4)
+ }
+
+ // Add a new table. But make sure the key hasn't already been used
+ // for something else.
+ if hash, ok := hashContext[k].([]map[string]interface{}); ok {
+ hashContext[k] = append(hash, make(map[string]interface{}))
+ } else {
+ p.panicf("Key '%s' was already created and cannot be used as an array.", key)
+ }
+ } else {
+ p.setValue(key[len(key)-1], make(map[string]interface{}))
+ }
+ p.context = append(p.context, key[len(key)-1])
+}
+
+// set calls setValue and setType.
+func (p *parser) set(key string, val interface{}, typ tomlType, pos Position) {
+ p.setValue(key, val)
+ p.setType(key, typ, pos)
+}
+
+// setValue sets the given key to the given value in the current context.
+// It will make sure that the key hasn't already been defined, account for
+// implicit key groups.
+func (p *parser) setValue(key string, value interface{}) {
+ var (
+ tmpHash interface{}
+ ok bool
+ hash = p.mapping
+ keyContext Key
+ )
+ for _, k := range p.context {
+ keyContext = append(keyContext, k)
+ if tmpHash, ok = hash[k]; !ok {
+ p.bug("Context for key '%s' has not been established.", keyContext)
+ }
+ switch t := tmpHash.(type) {
+ case []map[string]interface{}:
+ // The context is a table of hashes. Pick the most recent table
+ // defined as the current hash.
+ hash = t[len(t)-1]
+ case map[string]interface{}:
+ hash = t
+ default:
+ p.panicf("Key '%s' has already been defined.", keyContext)
+ }
+ }
+ keyContext = append(keyContext, key)
+
+ if _, ok := hash[key]; ok {
+ // Normally redefining keys isn't allowed, but the key could have been
+ // defined implicitly and it's allowed to be redefined concretely. (See
+ // the `valid/implicit-and-explicit-after.toml` in toml-test)
+ //
+ // But we have to make sure to stop marking it as an implicit. (So that
+ // another redefinition provokes an error.)
+ //
+ // Note that since it has already been defined (as a hash), we don't
+ // want to overwrite it. So our business is done.
+ if p.isArray(keyContext) {
+ p.removeImplicit(keyContext)
+ hash[key] = value
+ return
+ }
+ if p.isImplicit(keyContext) {
+ p.removeImplicit(keyContext)
+ return
+ }
+
+ // Otherwise, we have a concrete key trying to override a previous
+ // key, which is *always* wrong.
+ p.panicf("Key '%s' has already been defined.", keyContext)
+ }
+
+ hash[key] = value
+}
+
+// setType sets the type of a particular value at a given key. It should be
+// called immediately AFTER setValue.
+//
+// Note that if `key` is empty, then the type given will be applied to the
+// current context (which is either a table or an array of tables).
+func (p *parser) setType(key string, typ tomlType, pos Position) {
+ keyContext := make(Key, 0, len(p.context)+1)
+ keyContext = append(keyContext, p.context...)
+ if len(key) > 0 { // allow type setting for hashes
+ keyContext = append(keyContext, key)
+ }
+ // Special case to make empty keys ("" = 1) work.
+ // Without it it will set "" rather than `""`.
+ // TODO: why is this needed? And why is this only needed here?
+ if len(keyContext) == 0 {
+ keyContext = Key{""}
+ }
+ p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos}
+}
+
+// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
+// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
+func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} }
+func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) }
+func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok }
+func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray }
+func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) }
+
+// current returns the full key name of the current context.
+func (p *parser) current() string {
+ if len(p.currentKey) == 0 {
+ return p.context.String()
+ }
+ if len(p.context) == 0 {
+ return p.currentKey
+ }
+ return fmt.Sprintf("%s.%s", p.context, p.currentKey)
+}
+
+func stripFirstNewline(s string) string {
+ if len(s) > 0 && s[0] == '\n' {
+ return s[1:]
+ }
+ if len(s) > 1 && s[0] == '\r' && s[1] == '\n' {
+ return s[2:]
+ }
+ return s
+}
+
+// stripEscapedNewlines removes whitespace after line-ending backslashes in
+// multiline strings.
+//
+// A line-ending backslash is an unescaped \ followed only by whitespace until
+// the next newline. After a line-ending backslash, all whitespace is removed
+// until the next non-whitespace character.
+func (p *parser) stripEscapedNewlines(s string) string {
+ var b strings.Builder
+ var i int
+ for {
+ ix := strings.Index(s[i:], `\`)
+ if ix < 0 {
+ b.WriteString(s)
+ return b.String()
+ }
+ i += ix
+
+ if len(s) > i+1 && s[i+1] == '\\' {
+ // Escaped backslash.
+ i += 2
+ continue
+ }
+ // Scan until the next non-whitespace.
+ j := i + 1
+ whitespaceLoop:
+ for ; j < len(s); j++ {
+ switch s[j] {
+ case ' ', '\t', '\r', '\n':
+ default:
+ break whitespaceLoop
+ }
+ }
+ if j == i+1 {
+ // Not a whitespace escape.
+ i++
+ continue
+ }
+ if !strings.Contains(s[i:j], "\n") {
+ // This is not a line-ending backslash.
+ // (It's a bad escape sequence, but we can let
+ // replaceEscapes catch it.)
+ i++
+ continue
+ }
+ b.WriteString(s[:i])
+ s = s[j:]
+ i = 0
+ }
+}
+
+func (p *parser) replaceEscapes(it item, str string) string {
+ replaced := make([]rune, 0, len(str))
+ s := []byte(str)
+ r := 0
+ for r < len(s) {
+ if s[r] != '\\' {
+ c, size := utf8.DecodeRune(s[r:])
+ r += size
+ replaced = append(replaced, c)
+ continue
+ }
+ r += 1
+ if r >= len(s) {
+ p.bug("Escape sequence at end of string.")
+ return ""
+ }
+ switch s[r] {
+ default:
+ p.bug("Expected valid escape code after \\, but got %q.", s[r])
+ case ' ', '\t':
+ p.panicItemf(it, "invalid escape: '\\%c'", s[r])
+ case 'b':
+ replaced = append(replaced, rune(0x0008))
+ r += 1
+ case 't':
+ replaced = append(replaced, rune(0x0009))
+ r += 1
+ case 'n':
+ replaced = append(replaced, rune(0x000A))
+ r += 1
+ case 'f':
+ replaced = append(replaced, rune(0x000C))
+ r += 1
+ case 'r':
+ replaced = append(replaced, rune(0x000D))
+ r += 1
+ case 'e':
+ if p.tomlNext {
+ replaced = append(replaced, rune(0x001B))
+ r += 1
+ }
+ case '"':
+ replaced = append(replaced, rune(0x0022))
+ r += 1
+ case '\\':
+ replaced = append(replaced, rune(0x005C))
+ r += 1
+ case 'x':
+ if p.tomlNext {
+ escaped := p.asciiEscapeToUnicode(it, s[r+1:r+3])
+ replaced = append(replaced, escaped)
+ r += 3
+ }
+ case 'u':
+ // At this point, we know we have a Unicode escape of the form
+ // `uXXXX` at [r, r+5). (Because the lexer guarantees this
+ // for us.)
+ escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5])
+ replaced = append(replaced, escaped)
+ r += 5
+ case 'U':
+ // At this point, we know we have a Unicode escape of the form
+ // `uXXXX` at [r, r+9). (Because the lexer guarantees this
+ // for us.)
+ escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9])
+ replaced = append(replaced, escaped)
+ r += 9
+ }
+ }
+ return string(replaced)
+}
+
+func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune {
+ s := string(bs)
+ hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
+ if err != nil {
+ p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err)
+ }
+ if !utf8.ValidRune(rune(hex)) {
+ p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s)
+ }
+ return rune(hex)
+}
diff --git a/testdata/ja-JP.toml b/testdata/ja-JP.toml
new file mode 100644
index 0000000..bc2712c
--- /dev/null
+++ b/testdata/ja-JP.toml
@@ -0,0 +1,1221 @@
+[__meta__]
+ comments = ""
+ generated = 2021-11-29T19:38:18Z
+ language = "ja-JP"
+ maintainers = [""]
+
+["button/add-new"]
+ default = "新規追加"
+
+["button/add-user"]
+ default = "新規ユーザー追加"
+
+["button/cfg-dashboard"]
+ default = "ダッシュボードレイアウトを設定"
+
+["button/change"]
+ default = "変更"
+
+["button/change-passwd"]
+ default = "パスワード変更"
+
+["button/copy"]
+ default = "コピー"
+
+["button/delete"]
+ default = "削除"
+
+["button/delete-account"]
+ default = "アカウント・すべてのサイト・すべてのデータを削除"
+
+["button/delete-all"]
+ default = "はい、すべてを削除します!"
+
+["button/delete-everything"]
+ default = "はい、すべてを削除します"
+
+["button/disable-mfa"]
+ default = "MFA を無効化"
+
+["button/edit"]
+ default = "編集"
+
+["button/enable-mfa"]
+ default = "MFA を有効化"
+
+["button/forgot-password"]
+ default = "パスワードをお忘れですか?"
+
+["button/remove"]
+ default = "削除"
+
+["button/request-reset"]
+ default = "パスワードリセットをリクエスト"
+
+["button/resend-email"]
+ default = "メールを再送"
+
+["button/reset-defaults"]
+ default = "デフォルトにリセット"
+
+["button/reset-password"]
+ default = "パスワードリセット"
+
+["button/rm-hits"]
+ default = "ページビューを削除"
+
+["button/save"]
+ default = "保存"
+
+["button/save-default-view"]
+ default = "デフォルトビューを保存"
+
+["button/send-login-url"]
+ default = "ログイン URL を送信"
+
+["button/sign-in"]
+ default = "サインイン"
+
+["button/start-export"]
+ default = "エクスポート開始"
+
+["button/start-import"]
+ default = "インポート開始"
+
+["button/submit"]
+ default = "送信"
+
+["confirm/delete-user"]
+ default = "%(email) を削除しますか?"
+
+["dashboard/day-ago"]
+ default = "%(n) 日前"
+
+["dashboard/future"]
+ default = "未来"
+
+["dashboard/month-ago"]
+ default = "%(n) か月前"
+
+["dashboard/nothing-to-display"]
+ default = "表示するものがありません"
+
+["dashboard/pages/header"]
+ default = "ページ"
+
+["dashboard/pages/num-visits"]
+ default = "%(total-visits) 回の訪問のうち %(num-visits) 回を表示"
+
+["dashboard/pages/pageviews"]
+ default = "ページビュー"
+
+["dashboard/pages/path"]
+ default = "パス"
+
+["dashboard/pages/stats"]
+ default = "統計"
+
+["dashboard/pages/stats-tooltip"]
+ default = "各バーは選択された表示期間の 1/12 を表します"
+
+["dashboard/pages/title"]
+ default = "タイトル"
+
+["dashboard/pages/views"]
+ default = "ビュー"
+
+["dashboard/pages/visits"]
+ default = "訪問"
+
+["dashboard/today"]
+ default = "今日"
+
+["dashboard/tooltip-event"]
+ default = "%(unique) 回のクリック、%(clicks) 回の合計クリック"
+
+["dashboard/totals/header"]
+ default = "合計"
+
+["dashboard/totals/num-visits"]
+ default = "%(num-visits) 回の訪問"
+
+["dashboard/week-ago"]
+ default = "%(n) 週間前"
+
+["dashboard/yesterday"]
+ default = "昨日"
+
+["data-collect/help/country"]
+ default = "国名 (例: Belgium、Indonesia など)"
+
+["data-collect/help/language"]
+ default = "Accept-Languageで対応可能な言語"
+
+["data-collect/help/referrer"]
+ default = "Referer ヘッダと Campaign パラメータ"
+
+["data-collect/help/region"]
+ default = "地域 (例: Texas、Bali など)、詳細は国によって異なります。"
+
+["data-collect/help/sessions"]
+ default = "ユニーク訪問者を最大 8 時間追跡します。この機能を無効にすると、例えば F5 キーを押してページを再読み込みした場合、1 ページビューではなく、2 ページビューとして表示されます。"
+
+["data-collect/help/size"]
+ default = "スクリーンサイズ"
+
+["data-collect/help/user-agent"]
+ default = "User-Agent ヘッダで、ブラウザとシステムの名前とバージョンを取得します。"
+
+["data-collect/label/country"]
+ default = "国"
+
+["data-collect/label/language"]
+ default = "言語"
+
+["data-collect/label/referrer"]
+ default = "Referrer"
+
+["data-collect/label/region"]
+ default = "地域"
+
+["data-collect/label/sessions"]
+ default = "セッション"
+
+["data-collect/label/size"]
+ default = "サイズ"
+
+["data-collect/label/user-agent"]
+ default = "User-Agent"
+
+["datepicker/keyboard"]
+ default = "矢印キーで日付を選ぶ"
+
+["datepicker/month-next"]
+ default = "次の月"
+
+["datepicker/month-prev"]
+ default = "前の月"
+
+["email/header"]
+ default = "こんにちは、"
+
+["email/password-reset"]
+ default = """
+誰か (おそらくあなた) が GoatCounter アカウントのパスワードリセットをリクエストしました。
+
+リンクはこちらです:
+%(link)
+"""
+
+["email/reset-user-email-subject"]
+ default = "%(domain) のパスワードリセット"
+
+["email/signature"]
+ default = """
+何らかの問題・質問・コメントなどがある場合は、このメールに返信してください。
+
+よろしくお願いします。
+
+Martin
+"""
+
+["error/account-has-stripe-subscription"]
+ default = "このアカウントには、Stripe サブスクリプションがあるので、まず課金ページでそれをキャンセルしてください。"
+
+["error/address-exists"]
+ default = "%(addr) はすでに存在します"
+
+["error/could-not-read"]
+ default = "gzip として読み込めませんでした: %(err)"
+
+["error/date-future"]
+ default = "これは未来です"
+
+["error/date-mismatch"]
+ default = "終了日が開始日より前です"
+
+["error/date-past"]
+ default = "これはサイトの作成前です"
+
+["error/delete-main-site"]
+ default = "メインサイトは削除できません"
+
+["error/export-expired"]
+ default = "まだエクスポートが行われていないか、エクスポートの有効期限が切れているようです。"
+
+["error/incorrect-password"]
+ default = "現在のパスワードが正しくありません。"
+
+["error/invalid-end-date"]
+ default = "無効な終了日: %(date)"
+
+["error/invalid-start-date"]
+ default = "無効な開始日: %(date)"
+
+["error/load-url"]
+ default = "%(url) を読み込めませんでした: %(error)"
+
+["error/login-invalid"]
+ default = "無効なログインです"
+
+["error/login-no-password"]
+ default = "%(email) にパスワードが設定されていません。リセットしてください。"
+
+["error/login-not-found"]
+ default = "ユーザー %(email) が見つかりません"
+
+["error/login-token-expired"]
+ default = "指定されたトークンのユーザーが見つかりませんでした。有効期限切れまたはすでに使用されていませんか?"
+
+["error/login-wrong-pwd"]
+ default = "%(email) のパスワードが間違っています"
+
+["error/not-found"]
+ default = "見つかりません"
+
+["error/password-does-not-match"]
+ default = "パスワードの確認が一致していません。"
+
+["error/payment-cancelled"]
+ default = "支払いがキャンセルされました。"
+
+["error/reset-user-no-account"]
+ default = "このサイトのアカウントではありません: %(email)"
+
+["error/token-already-used"]
+ default = "不明なトークンです。すでに使用されていませんか?"
+
+["error/wrong-verification-key"]
+ default = "検証キーが間違っています。"
+
+["event"]
+ default = "イベント"
+
+["forgot-domain-help"]
+ default = "メールアドレスに関連付けられているすべてのドメインの一覧をメールで送信します。"
+
+["header/access"]
+ default = "アクセス"
+
+["header/add-new-user"]
+ default = "新規ユーザー追加"
+
+["header/allow-access"]
+ default = "許可するアクセス"
+
+["header/api"]
+ default = "API"
+
+["header/api-tokens"]
+ default = "API トークン"
+
+["header/browsers"]
+ default = "ブラウザ"
+
+["header/change-code"]
+ default = "サイトコード変更"
+
+["header/change-passwd"]
+ default = "パスワード変更"
+
+["header/code"]
+ default = "コード"
+
+["header/copy-settings"]
+ default = "設定をコピー"
+
+["header/created-at"]
+ default = "作成:"
+
+["header/dashboard"]
+ default = "ダッシュボード"
+
+["header/data-collection"]
+ default = "収集する情報"
+
+["header/delete-account"]
+ default = "アカウント削除"
+
+["header/domain"]
+ default = "ドメイン"
+
+["header/domain-settings"]
+ default = "ドメイン設定"
+
+["header/edit-user"]
+ default = "ユーザー %(email) を編集"
+
+["header/email"]
+ default = "メールアドレス"
+
+["header/export"]
+ default = "エクスポート"
+
+["header/export-or-import"]
+ default = "インポート/エクスポート"
+
+["header/finished"]
+ default = "完了"
+
+["header/forgot-domain"]
+ default = "ドメインを忘れました"
+
+["header/forgot-password"]
+ default = "パスワードを忘れました"
+
+["header/hash"]
+ default = "ハッシュ"
+
+["header/import"]
+ default = "インポート"
+
+["header/l10n"]
+ default = "ローカライゼーション"
+
+["header/languages"]
+ default = "言語"
+
+["header/last-10-exports"]
+ default = "過去 10 回のエクスポート"
+
+["header/locations"]
+ default = "位置"
+
+["header/locations-for"]
+ default = "%(country) の位置"
+
+["header/mfa"]
+ default = "多要素認証"
+
+["header/n-hits"]
+ default = "# ヒット数"
+
+["header/name"]
+ default = "名前"
+
+["header/new"]
+ default = "新規"
+
+["header/pagination-cursor"]
+ default = "ページネーションカーソル"
+
+["header/passwd-mfa"]
+ default = "パスワードと MFA"
+
+["header/path"]
+ default = "パス"
+
+["header/permissions"]
+ default = "権限"
+
+["header/preferences"]
+ default = "設定"
+
+["header/reset-password"]
+ default = "%(site-name) の %(email) のパスワードをリセット"
+
+["header/rm-hits"]
+ default = "ページビューを削除"
+
+["header/settings"]
+ default = "設定"
+
+["header/sign-in-at"]
+ default = "%(name) でサインイン"
+
+["header/site-settings"]
+ default = "サイト設定"
+
+["header/sites"]
+ default = "サイト"
+
+["header/size"]
+ default = "サイズ"
+
+["header/sizes"]
+ default = "サイズ"
+
+["header/start-pagination-cursor"]
+ default = "開始地点のページネーションカーソル"
+
+["header/started"]
+ default = "開始"
+
+["header/systems"]
+ default = "システム"
+
+["header/title"]
+ default = "タイトル"
+
+["header/token"]
+ default = "トークン"
+
+["header/toprefs"]
+ default = "上位の Referrer"
+
+["header/tracking"]
+ default = "トラッキング"
+
+["header/updated"]
+ default = "更新"
+
+["header/user-info"]
+ default = "ユーザー情報"
+
+["header/user-information"]
+ default = "ユーザー情報"
+
+["header/users"]
+ default = "ユーザー"
+
+["help/allow-visitor-counts"]
+ default = "使い方の詳細は%[ドキュメント]をご覧ください。"
+
+["help/campaign-parameters"]
+ default = "「Campaign」としてカウントするパラメータ一覧です。設定された場合、その値が Referrer として設定され Referer ヘッダが上書きされます。"
+
+["help/cfg-dashboard"]
+ default = "ダッシュボードに表示する内容や順番を変更します。"
+
+["help/code-access"]
+ default = "https://<em>[コード]</em>.%(domain) からアクセスします。"
+
+["help/custom-domain"]
+ default = "カスタムドメイン (例: <em>stats.example.com</em>)。<strong>注意:</strong> この機能を使用してもほとんどの広告ブロッカーによる GoatCounter のブロックは阻止<em>できません</em>。この機能は「バニティドメイン」としての使用を意図しています。"
+
+["help/custom-domain-cname"]
+ default = "<code>%(domain)</code> への CNAME レコードを設定します。 – %[%docs 詳細情報]"
+
+["help/custom-domain-error"]
+ default = """
+%[%error 未検証]。<code>%(domain)</code> への CNAME レコードを設定します。 – %[%docs 詳細情報]
+検証は 2 時間ごとに実行されます。
+"""
+
+["help/custom-domain-plan"]
+ default = "Personal Plus または Business プランが必要です (あなたは現在 %(plan) プランです、%[%link 課金])。"
+
+["help/custom-domain-verified"]
+ default = "ドメインの検証と設定です (注意: 証明書が機能するまでに 1 時間程度かかる場合があります)。"
+
+["help/data-retention"]
+ default = "ページビューと関連するすべてのデータは指定された日数経過後に完全に削除されます。<code>0</code> に設定すると永久に削除されません。"
+
+["help/domain-access"]
+ default = "このサイトにアクセスするためのドメインです。"
+
+["help/drag-reorder"]
+ default = "ドラッグして並び替え"
+
+["help/for-the-following-countries"]
+ default = "国名コードの一覧 (%[一覧]、alpha-2 コードを使用)。空白のままにすると、すべての国について収集します (有効な場合)。"
+
+["help/goatcounter-domain"]
+ default = "あなたの GoatCounter インストールドメイン (例: <em>stats.example.com</em>)。"
+
+["help/ignore-ips"]
+ default = "ここで指定した IP アドレスからのリクエストをカウントしません。カンマで区切ります。完全一致にのみ対応します。%[現在の IP を追加]。"
+
+["help/ignore-ips-2"]
+ default = "または、%[このブラウザでは無効にする] (もう一度クリックすると有効に戻ります)。"
+
+["help/new-user-email"]
+ default = "ログインに使用するメールです。"
+
+["help/new-user-email-confirm"]
+ default = "ログインに使用するメールです。確認する必要があります。"
+
+["help/no-undo"]
+ default = "これは破壊的な操作であり、元には戻せません!"
+
+["help/password-edit"]
+ default = "空欄のままにすると、変更されません。"
+
+["help/password-new-user"]
+ default = "パスワードリセットのメールを送信する場合は、空白にしてください。"
+
+["help/public"]
+ default = "ダッシュボードを閲覧可能な人を設定します。"
+
+["help/rm-hits"]
+ default = "削除前にプレビューが表示されます"
+
+["help/save-default-view"]
+ default = "現在のビュー (黄色のボックス内のすべての設定) を未選択時に読み込むデフォルトとして保存します。"
+
+["help/turing-test"]
+ default = "あなたが人間であることのちょっとした確認です"
+
+["help/your-email"]
+ default = "アドレスを変更した場合は、再度検証が必要です。"
+
+["label/24-hour-clock"]
+ default = "24 時間表示 (13:00)"
+
+["label/add-new"]
+ default = "新規追加"
+
+["label/all-sites"]
+ default = "すべてのサイト"
+
+["label/allow-admin-access"]
+ default = "管理者アクセスを許可"
+
+["label/allow-visitor-counts"]
+ default = "ウェブサイトへの訪問者カウントの追加を許可"
+
+["label/browser-stats"]
+ default = "ブラウザ統計"
+
+["label/campaign-parameters"]
+ default = "Campaign パラメータ"
+
+["label/change-code"]
+ default = "https://<em>[コード]</em>.%(domain) からアカウントにアクセスします。 – %[%link 変更]"
+
+["label/clear-pageviews"]
+ default = "既存のページビューをすべて削除"
+
+["label/code"]
+ default = "コード"
+
+["label/collected-since"]
+ default = "2021 年 12 月 2 日以降に収集"
+
+["label/csv-compress-format"]
+ default = "CSV ファイル (gzip 圧縮も可)"
+
+["label/current-passwd"]
+ default = "現在のパスワード"
+
+["label/custom-domain"]
+ default = "カスタムドメイン"
+
+["label/dashboard-public"]
+ default = "ダッシュボードを閲覧可能な人"
+
+["label/data-retention"]
+ default = "データの保持日数"
+
+["label/date-fmt"]
+ default = "日付の形式"
+
+["label/delete-account-confirmation"]
+ default = "アカウント全体を削除してよろしいですか?"
+
+["label/delete-account-contact"]
+ default = "ここにチェック入れると、フォローアップの質問やコメントで連絡するかもしれません。私は通信事業者ではないのであなたを説得したりはしませんが、いくつか質問をしたり、不足している機能に関する将来の計画を説明したりする場合があります。"
+
+["label/delete-account-follow-up"]
+ default = "フォローアップを許可"
+
+["label/delete-account-reason"]
+ default = "GoatCounter に足りないもの、またはアカウントを削除したい理由をお教えいただけると幸いです。これは完全に任意です。"
+
+["label/delete-account-reason-placeholder"]
+ default = "削除の理由 (任意)"
+
+["label/email"]
+ default = "メールアドレス"
+
+["label/email-address"]
+ default = "メールアドレス"
+
+["label/for-following-countries"]
+ default = "以下の国のみ対象:"
+
+["label/goatcounter-domain"]
+ default = "GoatCounter ドメイン"
+
+["label/ignore-ips"]
+ default = "無視する IP"
+
+["label/lang"]
+ default = "言語"
+
+["label/language-stats"]
+ default = "言語統計"
+
+["label/loc-stats"]
+ default = "位置統計"
+
+["label/mark-current"]
+ default = "(現在)"
+
+["label/match-title"]
+ default = "タイトルも含める"
+
+["label/mfa-token"]
+ default = "MFA トークン"
+
+["label/new-code"]
+ default = "新規コード"
+
+["label/new-password"]
+ default = "新規パスワード"
+
+["label/new-password-confirm"]
+ default = "新規パスワード (確認)"
+
+["label/pagination-cursor"]
+ default = "ページネーションカーソル"
+
+["label/password"]
+ default = "パスワード"
+
+["label/paths"]
+ default = "パス概要"
+
+["label/public-anyone"]
+ default = "誰でも"
+
+["label/public-private"]
+ default = "ログインユーザーのみ"
+
+["label/public-token"]
+ default = "ログインユーザーまたはシークレットトークンを知っている人"
+
+["label/secret"]
+ default = "シークレット"
+
+["label/secret-access"]
+ default = "シークレットアクセス URL:"
+
+["label/secret-token"]
+ default = "シークレットトークン"
+
+["label/set-default"]
+ default = "新規ユーザーとパブリックビュー (有効な場合) のデフォルトとして設定"
+
+["label/size-desktop"]
+ default = "コンピュータモニター"
+
+["label/size-desktophd"]
+ default = "HD より大きいコンピュータモニター"
+
+["label/size-largephones"]
+ default = "大きな携帯電話・小さなタブレット"
+
+["label/size-phones"]
+ default = "携帯電話"
+
+["label/size-stats"]
+ default = "サイズ統計"
+
+["label/size-tablets"]
+ default = "タブレット・小さなラップトップ"
+
+["label/system-stats"]
+ default = "システム統計"
+
+["label/thousand-separator"]
+ default = "区切り文字"
+
+["label/timezone"]
+ default = "タイムゾーン"
+
+["label/topref"]
+ default = "上位の Referral"
+
+["label/total-pageviews"]
+ default = "合計サイトページビュー"
+
+["label/turing-test"]
+ default = "ここに 9 を入力してください"
+
+["label/verifictation-token"]
+ default = "検証トークン"
+
+["label/week-start"]
+ default = "週はじめを日曜日にする"
+
+["label/your-email"]
+ default = "あなたのメールアドレス"
+
+["label/your-site"]
+ default = "あなたのサイト"
+
+["link/add-translation"]
+ default = "翻訳を追加・更新"
+
+["link/api"]
+ default = "API"
+
+["link/api-docs"]
+ default = "API ドキュメント"
+
+["link/billing"]
+ default = "課金"
+
+["link/dashboard"]
+ default = "ダッシュボード"
+
+["link/generate-random"]
+ default = "ランダムなシークレットを生成"
+
+["link/goto-path"]
+ default = "%(path) を開く"
+
+["link/import"]
+ default = "インポート"
+
+["link/passwd-mfa"]
+ default = "パスワードと MFA"
+
+["link/preferences"]
+ default = "設定"
+
+["link/rm-account"]
+ default = "アカウント削除"
+
+["link/rm-views"]
+ default = "ページビュー削除"
+
+["link/set-from-browser"]
+ default = "ブラウザから設定"
+
+["link/settings"]
+ default = "設定"
+
+["link/show-more"]
+ default = "さらに表示"
+
+["link/sites"]
+ default = "サイト"
+
+["link/users"]
+ default = "ユーザー"
+
+["nav-bot/contact"]
+ default = "連絡先"
+
+["nav-bot/contribute"]
+ default = "貢献"
+
+["nav-bot/docs"]
+ default = "ドキュメント"
+
+["nav-bot/home"]
+ default = "ホーム"
+
+["nav-bot/src"]
+ default = "ソースコード"
+
+["nav-dash/back"]
+ default = "戻る"
+
+["nav-dash/by-day"]
+ default = "日別で表示"
+
+["nav-dash/current"]
+ default = "この"
+
+["nav-dash/day"]
+ default = "1 日"
+
+["nav-dash/end-date"]
+ default = "表示期間の終了日"
+
+["nav-dash/filter"]
+ default = "パスで絞り込む"
+
+["nav-dash/filter-tooltip"]
+ default = "パスとタイトルを大文字小文字の区別なしで絞り込みます"
+
+["nav-dash/forced-daily"]
+ default = "表示期間が 90 日を超える場合、時刻は表示できません"
+
+["nav-dash/forward"]
+ default = "進む"
+
+["nav-dash/half-year"]
+ default = "半年"
+
+["nav-dash/last"]
+ default = "過去"
+
+["nav-dash/month"]
+ default = "1 か月"
+
+["nav-dash/quarter"]
+ default = "3 か月"
+
+["nav-dash/start-date"]
+ default = "表示期間の開始日"
+
+["nav-dash/week"]
+ default = "1 週間"
+
+["nav-dash/year"]
+ default = "1 年"
+
+["no-title"]
+ default = "タイトルなし"
+
+["notify/add-one-thing"]
+ default = "少なくとも 1 つは追加する必要があります。でないと、ダッシュボードに表示するものがありません。"
+
+["notify/api-token-created"]
+ default = "API トークンが作成されました。"
+
+["notify/api-token-removed"]
+ default = "API トークンが削除されました。"
+
+["notify/disabled-multi-factor-auth"]
+ default = "多要素認証が無効になりました。"
+
+["notify/email-already-verified"]
+ default = "%(email) は検証されました。"
+
+["notify/export-started-in-background"]
+ default = "バックグラウンドでエクスポートが開始されました。完了すると、ダウンロードリンクが記載されたメールが届きます。"
+
+["notify/import-started-in-background"]
+ default = "バックグラウンドでインポートが開始されました。完了すると、メールが届きます。"
+
+["notify/login-after-password-reset"]
+ default = "パスワードがリセットされました。新しいパスワードでログインしてください。"
+
+["notify/multi-factor-auth-enabled"]
+ default = "多要素認証が有効になりました。"
+
+["notify/need-business-plan-custom-domain"]
+ default = "カスタムドメインを設定するには Business プランが必要です"
+
+["notify/need-email-verification-for-api"]
+ default = "API を利用する前に、メールアドレスの検証が必要です。"
+
+["notify/no-user-for-token"]
+ default = "指定されたトークンのユーザーが見つかりました。有効期限切れまたはすでに使用されていませんか?"
+
+["notify/not-found"]
+ default = "見つかりません"
+
+["notify/password-changed"]
+ default = "パスワードが変更されました。"
+
+["notify/payment-processed"]
+ default = "支払いプロセスが正常に完了しました。"
+
+["notify/payment-processing"]
+ default = "支払い処理業者から成功の報告を受けましたが、まだ支払い処理中です"
+
+["notify/reset-to-default"]
+ default = "デフォルトにリセットしました"
+
+["notify/reset-user-sent"]
+ default = "%(email) にメールを送信します"
+
+["notify/restored-previously-deleted-site"]
+ default = "「%(url)」は以前削除されましたが、すべてのデータとともにサイトを復元しました。"
+
+["notify/saved"]
+ default = "保存されました"
+
+["notify/sent-to-email"]
+ default = "%(email) に送信します。"
+
+["notify/settings-copied-to-site"]
+ default = "選択したサイトに設定がコピーされました。"
+
+["notify/site-added"]
+ default = "サイト「%(url)」が追加されました。"
+
+["notify/site-removed"]
+ default = "サイト「%(url)」が削除されました。"
+
+["notify/started-background-process"]
+ default = "バックグラウンドで起動します。完全に処理されるまでに 10-20 秒程度かかる場合があります。"
+
+["notify/user-added"]
+ default = "ユーザー「%(email)」が追加されました。"
+
+["notify/user-removed"]
+ default = "ユーザー「%(email)」が削除されました。"
+
+["notify/users-edited"]
+ default = "ユーザー「%(email)」が編集されました。"
+
+["p/add-goatcounter-to-multiple-websites"]
+ default = """
+<p>新規サイトを作成し、複数のウェブサイトに GoatCounter を追加します。すべてのサイトは同じプラン、ユーザー、ログインを共有しますが、それ以外は完全に分離されます。作成時に現在のサイトの設定がコピーされますが、その後はそれぞれが独立した設定となります。</p>
+
+<p>追加する数に制限はありません。</p>
+"""
+
+["p/additional-errors"]
+ default = "その他のエラー"
+
+["p/api-intro"]
+ default = "GoatCounter には限られた API が付属しています。現在、あなたは API からページビューのカウント、サイトの作成、サイトの削除、サイトの編集、エクスポートの作成が行えます。"
+
+["p/change-code-request"]
+ default = """
+<p>サイトコードとログインドメインを変更します。</p>
+
+<p><strong>警告:</strong> これは、<strong>即座に</strong>適用され古いコードは誰でも再登録できます。使用中のサイトでは、速やかに変更するか、一時的に 2 つのインテグレーションコード (古いコードと新しいコード) を追加してページビューの損失を防いでください。</p>
+
+<p>現在のコード: <code>%(current-code)</code> (%(current-url))</p>
+"""
+
+["p/change-email"]
+ default = "%[設定] でメールアドレスを変更します。"
+
+["p/collect-disabled"]
+ default = "現在、この情報の収集は%[設定で無効]になっています。"
+
+["p/copy-settings-from-current-site"]
+ default = "ドメイン名を除くすべての設定を現在のサイトからコピーします。"
+
+["p/csv-file-format"]
+ default = "CSV ファイルの形式については%[こちらのドキュメント]をご覧ください。"
+
+["p/delete-account-multi-site"]
+ default = "サイトと関連するすべてのサイトは削除されたとマークされアクセスできなくなりますが、データは即座には削除されません。7 日後に、すべてのデータは完全に削除されます。"
+
+["p/delete-account-one-site"]
+ default = "サイトは削除されたとマークされアクセスできなくなりますが、データは即座には削除されません。7 日後に、すべてのデータは完全に削除されます。"
+
+["p/disable-mfa"]
+ default = "このアカウントでは、現在 MFA が有効です。"
+
+["p/enable-mfa"]
+ default = "TOTP ベースの多要素認証 (MFA) を有効にするには、認証アプリでコードをスキャンするかシークレットを手動で入力してください。"
+
+["p/error"]
+ default = "エラー: %(error-message)"
+
+["p/export-process"]
+ default = """
+<p>プロセスを開始し、完了後にダウンロードリンクをメールで送信します。この操作は 1 時間に 1 回のみ可能で、以前のバックアップは上書きされます。</p>
+
+<p>これには、概要には表示されない「ボット」とマークされたものを含むすべてのページビューが含まれます。</p>
+"""
+
+["p/have-mfa"]
+ default = "このアカウントは多要素認証で保護されています。認証アプリに表示されているコードを入力してください。"
+
+["p/last-user"]
+ default = "唯一の管理者ユーザーのため削除・編集できません"
+
+["p/no-data"]
+ default = """
+<p>%[%bold データ未受信] – GoatCounter はまだデータを受信していません。<br>
+使い方はとても簡単で、以下の JavaScript をページ上の任意の場所に追加するだけです:</p>
+
+%[%pre %(js_code)]
+
+<p>ページビューが表示されない場合は、広告ブロッカーが GoatCounter をブロックしていないか確認してください (%(domain) および/または gc.zgo.at ドメイン)。</p>
+
+<p>このメッセージは、データを受信すると消えます。詳細なドキュメントと既存のインテグレーションについては%[%link_docs サイトコード]をご覧ください。</p>
+"""
+
+["p/no-matches"]
+ default = "%(query) に一致するものはありません。"
+
+["p/notify-immediate-change"]
+ default = "即座に適用されます"
+
+["p/notify-pagination-cursor"]
+ default = "メールには「ページネーションカーソル」という項目がありますが、ここにそれを入力すると、前回のエクスポートの後に記録されたページビューのみエクスポートされます。"
+
+["p/notify-site-deletion"]
+ default = "%(number) 件のサイトが削除されます"
+
+["p/remove-site-confirm"]
+ default = """
+サイト %(sitename) を削除してよろしいですか?<br>
+これにより、<strong>関連するすべてのデータが削除されます</strong>。
+"""
+
+["p/remove-site-confirm-contact"]
+ default = "他のサイトへの統合や、新しいアカウントへの切り離しなどの操作を行いたい場合は%[ご連絡]ください。"
+
+["p/remove-site-confirm-current"]
+ default = """
+サイト %(sitename) を削除してよろしいですか?<br>
+これにより、<strong>関連するすべてのデータ</strong>と<strong>現在のサイトが削除されます</strong>。
+"""
+
+["p/request-data-recovery"]
+ default = "気が変わりデータの復元を希望する場合は 7 日以内に%[ご連絡]ください。"
+
+["p/rm-hits"]
+ default = "特定ページのページビューをすべて削除します。"
+
+["p/rm-hits-help"]
+ default = "大文字と小文字は区別されません。ワイルドカードとして <code>%</code> に対応しています (例: <code>/page%.html</code> は <code>/page</code> で始まり <code>.html</code> で終わるものすべてに一致します)。<code>_</code> は任意の文字に一致します (例: <code>_.html</code> は <code>a.html</code> や <code>b.html</code> に一致します)。特別な意味を持たない文字列としては <code>\\%</code> や <code>\\_</code> を使用できます。"
+
+["p/rm-pageview-match"]
+ default = "以下のパスが %(query) に一致します:"
+
+["p/setting-recovery-disabled-information"]
+ default = "無効に設定した項目の情報はページビュー記録後に破棄されます。復元する方法はありません。"
+
+["p/settings-all-sites"]
+ default = "これらの設定は、あなたがアクセスできるすべてのサイトに対して有効になります。"
+
+["p/site-domain-link-to-page"]
+ default = "あなたのサイトのドメイン (例: <em>www.example.com</em>) を設定してください。概要でページのリンクに使用されます。"
+
+["p/text-data-retention"]
+ default = "これには、データの保持と収集の設定も含まれます。"
+
+["p/verify-email"]
+ default = "%(email) に送信されるリンクをクリックし、メールを検証してください。%[%sup (なぜ?)]"
+
+["page-ranking"]
+ default = "ページランキング"
+
+["restricted-admin-access"]
+ default = "通常、管理者はあなたのサイトに「ログイン」できませんが、これを有効にすると「ログイン」できるようになります。サポートのために、これを有効にするよう求められる場合があります。"
+
+["scale-y"]
+ default = "Y 軸のスケールを最大にする"
+
+["top-nav/back"]
+ default = "戻る"
+
+["top-nav/dashboard"]
+ default = "ダッシュボード"
+
+["top-nav/need-js"]
+ default = "GoatCounter を正しく機能させるには JavaScript を有効にする必要があります。%(domain) による JavaScript の実行を許可してください。"
+
+["top-nav/public-link"]
+ default = "%(domain) の分析結果です。"
+
+["top-nav/public-time"]
+ default = "パブリックビューは 1 時間に 1 回更新されます。時刻はすべて %(timezone-name) (%(timezone-offset)) で表示されます。"
+
+["top-nav/settings"]
+ default = "設定"
+
+["top-nav/sign-in"]
+ default = "サインイン"
+
+["top-nav/sign-out"]
+ default = "サインアウト"
+
+["top-nav/site-code"]
+ default = "サイトコード"
+
+["top-nav/sites"]
+ default = "サイト:"
+
+["top-nav/updates"]
+ default = "更新"
+
+["top-nav/user"]
+ default = "ユーザー"
+
+["unknown"]
+ default = "(不明)"
+
+["validate/bool"]
+ default = "ブール型である必要があります"
+
+["validate/color"]
+ default = "有効なカラーコードである必要があります"
+
+["validate/contains"]
+ default = "%s という文字列を含めることはできません"
+
+["validate/date"]
+ default = "「%s」のような日付である必要があります"
+
+["validate/domain"]
+ default = "有効なドメインである必要があります"
+
+["validate/email"]
+ default = "有効なメールアドレスである必要があります"
+
+["validate/exclude"]
+ default = "「%s」できません"
+
+["validate/hostname"]
+ default = "有効なホスト名である必要があります"
+
+["validate/include"]
+ default = "「%s」のうちの 1 つである必要があります"
+
+["validate/int"]
+ default = "整数である必要があります"
+
+["validate/ip"]
+ default = "有効な IPv4 または IPv6 である必要があります"
+
+["validate/ipv4"]
+ default = "有効な IPv4 である必要があります"
+
+["validate/len-longer"]
+ default = "%d より長い必要があります"
+
+["validate/len-shorter"]
+ default = "%d より短い必要があります"
+
+["validate/phone"]
+ default = "有効な電話番号である必要があります"
+
+["validate/range-higher"]
+ default = "%d 以上である必要があります"
+
+["validate/range-lower"]
+ default = "%d 以下である必要があります"
+
+["validate/required"]
+ default = "設定する必要があります"
+
+["validate/url"]
+ default = "有効な URL である必要があります"
+
+["validate/utf8"]
+ default = "UTF-8 である必要があります"
+
+["widget-setting/help/align"]
+ default = "ページのグラフと揃うように左側に余白を追加します"
+
+["widget-setting/help/chart-style"]
+ default = "グラフの表示形式"
+
+["widget-setting/help/no-events"]
+ default = "合計の概要にイベントを含めません"
+
+["widget-setting/help/page-size"]
+ default = "表示するページの数"
+
+["widget-setting/help/ref-page-size"]
+ default = "パスをクリックしたときに読み込む Referrer の数"
+
+["widget-setting/help/regions"]
+ default = "国一覧の代わりに、国の地域を表示する"
+
+["widget-setting/label/align"]
+ default = "ページと合わせる"
+
+["widget-setting/label/chart-style"]
+ default = "グラフ形式"
+
+["widget-setting/label/no-events"]
+ default = "イベントを除外"
+
+["widget-setting/label/page-size"]
+ default = "ページサイズ"
+
+["widget-setting/label/ref-page-size"]
+ default = "Referrer ページサイズ"
+
+["widget-setting/label/regions"]
+ default = "表示地域"
+
+["widget-settings/bar-chart"]
+ default = "棒グラフ"
+
+["widget-settings/line-chart"]
+ default = "折れ線グラフ"
+
+["widget-settings/text-chart"]
+ default = "テキストテーブル"
+
+["y-scale"]
+ default = "Y 軸スケール"
diff --git a/toml_test.go b/toml_test.go
new file mode 100644
index 0000000..fe2db9f
--- /dev/null
+++ b/toml_test.go
@@ -0,0 +1,514 @@
+//go:build go1.16
+// +build go1.16
+
+package toml_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/BurntSushi/toml"
+ "github.com/BurntSushi/toml/internal/tag"
+ tomltest "github.com/BurntSushi/toml/internal/toml-test"
+)
+
+// Test if the error message matches what we want for invalid tests. Every slice
+// entry is tested with strings.Contains.
+//
+// Filepaths are glob'd
+var errorTests = map[string][]string{
+ "encoding/bad-utf8*": {"invalid UTF-8 byte"},
+ "encoding/utf16*": {"files cannot contain NULL bytes; probably using UTF-16"},
+ "string/multiline-escape-space": {`invalid escape: '\ '`},
+}
+
+// Test metadata; all keys listed as "keyname: type".
+var metaTests = map[string]string{
+ "implicit-and-explicit-after": `
+ a.b.c: Hash
+ a.b.c.answer: Integer
+ a: Hash
+ a.better: Integer
+ `,
+ "implicit-and-explicit-before": `
+ a: Hash
+ a.better: Integer
+ a.b.c: Hash
+ a.b.c.answer: Integer
+ `,
+ "key/case-sensitive": `
+ sectioN: String
+ section: Hash
+ section.name: String
+ section.NAME: String
+ section.Name: String
+ Section: Hash
+ Section.name: String
+ Section."μ": String
+ Section."Μ": String
+ Section.M: String
+ `,
+ "key/dotted": `
+ name.first: String
+ name.last: String
+ many.dots.here.dot.dot.dot: Integer
+ count.a: Integer
+ count.b: Integer
+ count.c: Integer
+ count.d: Integer
+ count.e: Integer
+ count.f: Integer
+ count.g: Integer
+ count.h: Integer
+ count.i: Integer
+ count.j: Integer
+ count.k: Integer
+ count.l: Integer
+ tbl: Hash
+ tbl.a.b.c: Float
+ a.few.dots: Hash
+ a.few.dots.polka.dot: String
+ a.few.dots.polka.dance-with: String
+ arr: ArrayHash
+ arr.a.b.c: Integer
+ arr.a.b.d: Integer
+ arr: ArrayHash
+ arr.a.b.c: Integer
+ arr.a.b.d: Integer
+ `,
+ "key/empty": `
+ "": String
+ `,
+ "key/quoted-dots": `
+ plain: Integer
+ "with.dot": Integer
+ plain_table: Hash
+ plain_table.plain: Integer
+ plain_table."with.dot": Integer
+ table.withdot: Hash
+ table.withdot.plain: Integer
+ table.withdot."key.with.dots": Integer
+ `,
+ "key/space": `
+ "a b": Integer
+ " c d ": Integer
+ " tbl ": Hash
+ " tbl "."\ttab\ttab\t": String
+ `,
+ "key/special-chars": "\n" +
+ "\"=~!@$^&*()_+-`1234567890[]|/?><.,;:'=\": Integer\n",
+
+ // TODO: "(albums): Hash" is missing; the problem is that this is an
+ // "implied key", which is recorded in the parser in implicits, rather than
+ // in keys. This is to allow "redefining" tables, for example:
+ //
+ // [a.b.c]
+ // answer = 42
+ // [a]
+ // better = 43
+ //
+ // However, we need to actually pass on this information to the MetaData so
+ // we can use it.
+ //
+ // Keys are supposed to be in order, for the above right now that's:
+ //
+ // (a).(b).(c): Hash
+ // (a).(b).(c).(answer): Integer
+ // (a): Hash
+ // (a).(better): Integer
+ //
+ // So if we want to add "(a).(b): Hash", where should this be in the order?
+ "table/array-implicit": `
+ albums.songs: ArrayHash
+ albums.songs.name: String
+ `,
+
+ // TODO: people and people.* listed many times; not entirely sure if that's
+ // what we want?
+ //
+ // It certainly causes problems, because keys is a slice, and types a map.
+ // So if array entry 1 differs in type from array entry 2 then that won't be
+ // recorded right. This related to the problem in the above comment.
+ //
+ // people: ArrayHash
+ //
+ // people[0]: Hash
+ // people[0].first_name: String
+ // people[0].last_name: String
+ //
+ // people[1]: Hash
+ // people[1].first_name: String
+ // people[1].last_name: String
+ //
+ // people[2]: Hash
+ // people[2].first_name: String
+ // people[2].last_name: String
+ "table/array-many": `
+ people: ArrayHash
+ people.first_name: String
+ people.last_name: String
+ people: ArrayHash
+ people.first_name: String
+ people.last_name: String
+ people: ArrayHash
+ people.first_name: String
+ people.last_name: String
+ `,
+ "table/array-nest": `
+ albums: ArrayHash
+ albums.name: String
+ albums.songs: ArrayHash
+ albums.songs.name: String
+ albums.songs: ArrayHash
+ albums.songs.name: String
+ albums: ArrayHash
+ albums.name: String
+ albums.songs: ArrayHash
+ albums.songs.name: String
+ albums.songs: ArrayHash
+ albums.songs.name: String
+ `,
+ "table/array-one": `
+ people: ArrayHash
+ people.first_name: String
+ people.last_name: String
+ `,
+ "table/array-table-array": `
+ a: ArrayHash
+ a.b: ArrayHash
+ a.b.c: Hash
+ a.b.c.d: String
+ a.b: ArrayHash
+ a.b.c: Hash
+ a.b.c.d: String
+ `,
+ "table/empty": `
+ a: Hash
+ `,
+ "table/keyword": `
+ true: Hash
+ false: Hash
+ inf: Hash
+ nan: Hash
+ `,
+ "table/names": `
+ a.b.c: Hash
+ a."b.c": Hash
+ a."d.e": Hash
+ a." x ": Hash
+ d.e.f: Hash
+ g.h.i: Hash
+ j."ʞ".l: Hash
+ x.1.2: Hash
+ `,
+ "table/no-eol": `
+ table: Hash
+ `,
+ "table/sub-empty": `
+ a: Hash
+ a.b: Hash
+ `,
+ "table/whitespace": `
+ "valid key": Hash
+ `,
+ "table/with-literal-string": `
+ a: Hash
+ a."\"b\"": Hash
+ a."\"b\"".c: Hash
+ a."\"b\"".c.answer: Integer
+ `,
+ "table/with-pound": `
+ "key#group": Hash
+ "key#group".answer: Integer
+ `,
+ "table/with-single-quotes": `
+ a: Hash
+ a.b: Hash
+ a.b.c: Hash
+ a.b.c.answer: Integer
+ `,
+ "table/without-super": `
+ x.y.z.w: Hash
+ x: Hash
+ `,
+}
+
+// TOML 1.0
+func TestToml(t *testing.T) {
+ runTomlTest(t, false)
+}
+
+// TOML 1.1
+func TestTomlNext(t *testing.T) {
+ toml.WithTomlNext(func() {
+ runTomlTest(t, true)
+ })
+}
+
+// Make sure TOML 1.1 fails by default for now.
+func TestTomlNextFails(t *testing.T) {
+ runTomlTest(t, true,
+ "valid/string/escape-esc",
+ "valid/datetime/no-seconds",
+ "valid/string/hex-escape",
+ "valid/inline-table/newline",
+ "valid/key/unicode")
+}
+
+func runTomlTest(t *testing.T, includeNext bool, wantFail ...string) {
+ for k := range errorTests { // Make sure patterns are valid.
+ _, err := filepath.Match(k, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // TODO: bit of a hack to make sure not all test run; without this "-run=.."
+ // will still run alll tests, but just report the errors for the -run value.
+ // This is annoying in cases where you have some debug printf.
+ //
+ // Need to update toml-test a bit to make this easier, but this good enough
+ // for now.
+ var runTests []string
+ for _, a := range os.Args {
+ if strings.HasPrefix(a, "-test.run=TestToml/") {
+ a = strings.TrimPrefix(a, "-test.run=TestToml/encode/")
+ a = strings.TrimPrefix(a, "-test.run=TestToml/decode/")
+ runTests = []string{a, a + "/*"}
+ break
+ }
+ }
+
+ // Make sure the keys in metaTests and errorTests actually exist; easy to
+ // make a typo and nothing will get tested.
+ var (
+ shouldExistValid = make(map[string]struct{})
+ shouldExistInvalid = make(map[string]struct{})
+ )
+ if len(runTests) == 0 {
+ for k := range metaTests {
+ shouldExistValid["valid/"+k] = struct{}{}
+ }
+ for k := range errorTests {
+ shouldExistInvalid["invalid/"+k] = struct{}{}
+ }
+ }
+
+ run := func(t *testing.T, enc bool) {
+ r := tomltest.Runner{
+ Files: tomltest.EmbeddedTests(),
+ Encoder: enc,
+ Parser: parser{},
+ RunTests: runTests,
+ SkipTests: []string{
+ // "15" in time.Parse() accepts both "1" and "01". The TOML
+ // specification says that times *must* start with a leading
+ // zero, but this requires writing out own datetime parser.
+ // I think it's actually okay to just accept both really.
+ // https://github.com/BurntSushi/toml/issues/320
+ "invalid/datetime/time-no-leads",
+
+ // These tests are fine, just doesn't deal well with empty output.
+ "valid/comment/noeol",
+ "valid/comment/nonascii",
+
+ // TODO: fix this; we allow appending to tables, but shouldn't.
+ "invalid/table/append-with-dotted*",
+ "invalid/inline-table/add",
+ "invalid/table/duplicate-key-dotted-table",
+ "invalid/table/duplicate-key-dotted-table2",
+ "invalid/spec/inline-table-2-0",
+ "invalid/spec/table-9-1",
+ "invalid/inline-table/nested_key_conflict",
+ "invalid/table/append-to-array-with-dotted-keys",
+ },
+ }
+ if includeNext {
+ r.Version = "next"
+ }
+
+ tests, err := r.Run()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ failed := make(map[string]struct{})
+ for _, test := range tests.Tests {
+ t.Run(test.Path, func(t *testing.T) {
+ if test.Failed() {
+ for _, f := range wantFail {
+ if f == test.Path {
+ failed[test.Path] = struct{}{}
+ return
+ }
+ }
+
+ t.Fatalf("\nError:\n%s\n\nInput:\n%s\nOutput:\n%s\nWant:\n%s\n",
+ test.Failure, test.Input, test.Output, test.Want)
+ return
+ }
+
+ // Test error message.
+ if test.Type() == tomltest.TypeInvalid {
+ testError(t, test, shouldExistInvalid)
+ }
+ // Test metadata
+ if !enc && test.Type() == tomltest.TypeValid {
+ delete(shouldExistValid, test.Path)
+ testMeta(t, test, includeNext)
+ }
+ })
+ }
+ for _, f := range wantFail {
+ if _, ok := failed[f]; !ok {
+ t.Errorf("expected test %q to fail but it didn't", f)
+ }
+ }
+
+ t.Logf("passed: %d; failed: %d; skipped: %d", tests.Passed, tests.Failed, tests.Skipped)
+ }
+
+ t.Run("decode", func(t *testing.T) { run(t, false) })
+ t.Run("encode", func(t *testing.T) { run(t, true) })
+
+ if len(shouldExistValid) > 0 {
+ var s []string
+ for k := range shouldExistValid {
+ s = append(s, k)
+ }
+ t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", "))
+ }
+ if len(shouldExistInvalid) > 0 {
+ var s []string
+ for k := range shouldExistInvalid {
+ s = append(s, k)
+ }
+ t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", "))
+ }
+}
+
+var reCollapseSpace = regexp.MustCompile(` +`)
+
+func testMeta(t *testing.T, test tomltest.Test, includeNext bool) {
+ want, ok := metaTests[strings.TrimPrefix(test.Path, "valid/")]
+ if !ok {
+ return
+ }
+
+ // Output is slightly different due to different quoting; just skip for now.
+ if includeNext && (test.Path == "valid/table/names" || test.Path == "valid/key/case-sensitive") {
+ return
+ }
+
+ var s interface{}
+ meta, err := toml.Decode(test.Input, &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b := new(strings.Builder)
+ for i, k := range meta.Keys() {
+ if i > 0 {
+ b.WriteByte('\n')
+ }
+ fmt.Fprintf(b, "%s: %s", k, meta.Type(k...))
+ }
+ have := b.String()
+
+ want = reCollapseSpace.ReplaceAllString(strings.ReplaceAll(strings.TrimSpace(want), "\t", ""), " ")
+ if have != want {
+ t.Errorf("MetaData wrong\nhave:\n%s\nwant:\n%s", have, want)
+ }
+}
+
+func testError(t *testing.T, test tomltest.Test, shouldExist map[string]struct{}) {
+ path := strings.TrimPrefix(test.Path, "invalid/")
+
+ errs, ok := errorTests[path]
+ if ok {
+ delete(shouldExist, "invalid/"+path)
+ }
+ if !ok {
+ for k := range errorTests {
+ ok, _ = filepath.Match(k, path)
+ if ok {
+ delete(shouldExist, "invalid/"+k)
+ errs = errorTests[k]
+ break
+ }
+ }
+ }
+ if !ok {
+ return
+ }
+
+ for _, e := range errs {
+ if !strings.Contains(test.Output, e) {
+ t.Errorf("\nwrong error message\nhave: %s\nwant: %s", test.Output, e)
+ }
+ }
+}
+
+type parser struct{}
+
+func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
+ defer func() {
+ if r := recover(); r != nil {
+ switch rr := r.(type) {
+ case error:
+ retErr = rr
+ default:
+ retErr = fmt.Errorf("%s", rr)
+ }
+ }
+ }()
+
+ var tmp interface{}
+ err := json.Unmarshal([]byte(input), &tmp)
+ if err != nil {
+ return "", false, err
+ }
+
+ rm, err := tag.Remove(tmp)
+ if err != nil {
+ return err.Error(), true, retErr
+ }
+
+ buf := new(bytes.Buffer)
+ err = toml.NewEncoder(buf).Encode(rm)
+ if err != nil {
+ return err.Error(), true, retErr
+ }
+
+ return buf.String(), false, retErr
+}
+
+func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
+ defer func() {
+ if r := recover(); r != nil {
+ switch rr := r.(type) {
+ case error:
+ retErr = rr
+ default:
+ retErr = fmt.Errorf("%s", rr)
+ }
+ }
+ }()
+
+ var d interface{}
+ if _, err := toml.Decode(input, &d); err != nil {
+ return err.Error(), true, retErr
+ }
+
+ j, err := json.MarshalIndent(tag.Add("", d), "", " ")
+ if err != nil {
+ return "", false, err
+ }
+ return string(j), false, retErr
+}
diff --git a/type_fields.go b/type_fields.go
new file mode 100644
index 0000000..254ca82
--- /dev/null
+++ b/type_fields.go
@@ -0,0 +1,242 @@
+package toml
+
+// Struct field handling is adapted from code in encoding/json:
+//
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the Go distribution.
+
+import (
+ "reflect"
+ "sort"
+ "sync"
+)
+
+// A field represents a single field found in a struct.
+type field struct {
+ name string // the name of the field (`toml` tag included)
+ tag bool // whether field has a `toml` tag
+ index []int // represents the depth of an anonymous field
+ typ reflect.Type // the type of the field
+}
+
+// byName sorts field by name, breaking ties with depth,
+// then breaking ties with "name came from toml tag", then
+// breaking ties with index sequence.
+type byName []field
+
+func (x byName) Len() int { return len(x) }
+
+func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
+
+func (x byName) Less(i, j int) bool {
+ if x[i].name != x[j].name {
+ return x[i].name < x[j].name
+ }
+ if len(x[i].index) != len(x[j].index) {
+ return len(x[i].index) < len(x[j].index)
+ }
+ if x[i].tag != x[j].tag {
+ return x[i].tag
+ }
+ return byIndex(x).Less(i, j)
+}
+
+// byIndex sorts field by index sequence.
+type byIndex []field
+
+func (x byIndex) Len() int { return len(x) }
+
+func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
+
+func (x byIndex) Less(i, j int) bool {
+ for k, xik := range x[i].index {
+ if k >= len(x[j].index) {
+ return false
+ }
+ if xik != x[j].index[k] {
+ return xik < x[j].index[k]
+ }
+ }
+ return len(x[i].index) < len(x[j].index)
+}
+
+// typeFields returns a list of fields that TOML should recognize for the given
+// type. The algorithm is breadth-first search over the set of structs to
+// include - the top struct and then any reachable anonymous structs.
+func typeFields(t reflect.Type) []field {
+ // Anonymous fields to explore at the current level and the next.
+ current := []field{}
+ next := []field{{typ: t}}
+
+ // Count of queued names for current level and the next.
+ var count map[reflect.Type]int
+ var nextCount map[reflect.Type]int
+
+ // Types already visited at an earlier level.
+ visited := map[reflect.Type]bool{}
+
+ // Fields found.
+ var fields []field
+
+ for len(next) > 0 {
+ current, next = next, current[:0]
+ count, nextCount = nextCount, map[reflect.Type]int{}
+
+ for _, f := range current {
+ if visited[f.typ] {
+ continue
+ }
+ visited[f.typ] = true
+
+ // Scan f.typ for fields to include.
+ for i := 0; i < f.typ.NumField(); i++ {
+ sf := f.typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous { // unexported
+ continue
+ }
+ opts := getOptions(sf.Tag)
+ if opts.skip {
+ continue
+ }
+ index := make([]int, len(f.index)+1)
+ copy(index, f.index)
+ index[len(f.index)] = i
+
+ ft := sf.Type
+ if ft.Name() == "" && ft.Kind() == reflect.Ptr {
+ // Follow pointer.
+ ft = ft.Elem()
+ }
+
+ // Record found field and index sequence.
+ if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
+ tagged := opts.name != ""
+ name := opts.name
+ if name == "" {
+ name = sf.Name
+ }
+ fields = append(fields, field{name, tagged, index, ft})
+ if count[f.typ] > 1 {
+ // If there were multiple instances, add a second,
+ // so that the annihilation code will see a duplicate.
+ // It only cares about the distinction between 1 or 2,
+ // so don't bother generating any more copies.
+ fields = append(fields, fields[len(fields)-1])
+ }
+ continue
+ }
+
+ // Record new anonymous struct to explore in next round.
+ nextCount[ft]++
+ if nextCount[ft] == 1 {
+ f := field{name: ft.Name(), index: index, typ: ft}
+ next = append(next, f)
+ }
+ }
+ }
+ }
+
+ sort.Sort(byName(fields))
+
+ // Delete all fields that are hidden by the Go rules for embedded fields,
+ // except that fields with TOML tags are promoted.
+
+ // The fields are sorted in primary order of name, secondary order
+ // of field index length. Loop over names; for each name, delete
+ // hidden fields by choosing the one dominant field that survives.
+ out := fields[:0]
+ 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
+ out = append(out, fi)
+ continue
+ }
+ dominant, ok := dominantField(fields[i : i+advance])
+ if ok {
+ out = append(out, dominant)
+ }
+ }
+
+ fields = out
+ sort.Sort(byIndex(fields))
+
+ return fields
+}
+
+// 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 embedding rules, modified by the presence of
+// TOML tags. 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 []field) (field, bool) {
+ // The fields are sorted in increasing index-length order. The winner
+ // must therefore be one with the shortest index length. Drop all
+ // longer entries, which is easy: just truncate the slice.
+ length := len(fields[0].index)
+ tagged := -1 // Index of first tagged field.
+ for i, f := range fields {
+ if len(f.index) > length {
+ fields = fields[:i]
+ break
+ }
+ if f.tag {
+ if tagged >= 0 {
+ // Multiple tagged fields at the same level: conflict.
+ // Return no field.
+ return field{}, false
+ }
+ tagged = i
+ }
+ }
+ if tagged >= 0 {
+ return fields[tagged], true
+ }
+ // All remaining fields have the same length. If there's more than one,
+ // we have a conflict (two fields named "X" at the same level) and we
+ // return no field.
+ if len(fields) > 1 {
+ return field{}, false
+ }
+ return fields[0], true
+}
+
+var fieldCache struct {
+ sync.RWMutex
+ m map[reflect.Type][]field
+}
+
+// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
+func cachedTypeFields(t reflect.Type) []field {
+ fieldCache.RLock()
+ f := fieldCache.m[t]
+ fieldCache.RUnlock()
+ if f != nil {
+ return f
+ }
+
+ // Compute fields without lock.
+ // Might duplicate effort but won't hold other computations back.
+ f = typeFields(t)
+ if f == nil {
+ f = []field{}
+ }
+
+ fieldCache.Lock()
+ if fieldCache.m == nil {
+ fieldCache.m = map[reflect.Type][]field{}
+ }
+ fieldCache.m[t] = f
+ fieldCache.Unlock()
+ return f
+}
diff --git a/type_toml.go b/type_toml.go
new file mode 100644
index 0000000..4e90d77
--- /dev/null
+++ b/type_toml.go
@@ -0,0 +1,70 @@
+package toml
+
+// tomlType represents any Go type that corresponds to a TOML type.
+// While the first draft of the TOML spec has a simplistic type system that
+// probably doesn't need this level of sophistication, we seem to be militating
+// toward adding real composite types.
+type tomlType interface {
+ typeString() string
+}
+
+// typeEqual accepts any two types and returns true if they are equal.
+func typeEqual(t1, t2 tomlType) bool {
+ if t1 == nil || t2 == nil {
+ return false
+ }
+ return t1.typeString() == t2.typeString()
+}
+
+func typeIsTable(t tomlType) bool {
+ return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
+}
+
+type tomlBaseType string
+
+func (btype tomlBaseType) typeString() string {
+ return string(btype)
+}
+
+func (btype tomlBaseType) String() string {
+ return btype.typeString()
+}
+
+var (
+ tomlInteger tomlBaseType = "Integer"
+ tomlFloat tomlBaseType = "Float"
+ tomlDatetime tomlBaseType = "Datetime"
+ tomlString tomlBaseType = "String"
+ tomlBool tomlBaseType = "Bool"
+ tomlArray tomlBaseType = "Array"
+ tomlHash tomlBaseType = "Hash"
+ tomlArrayHash tomlBaseType = "ArrayHash"
+)
+
+// typeOfPrimitive returns a tomlType of any primitive value in TOML.
+// Primitive values are: Integer, Float, Datetime, String and Bool.
+//
+// Passing a lexer item other than the following will cause a BUG message
+// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
+func (p *parser) typeOfPrimitive(lexItem item) tomlType {
+ switch lexItem.typ {
+ case itemInteger:
+ return tomlInteger
+ case itemFloat:
+ return tomlFloat
+ case itemDatetime:
+ return tomlDatetime
+ case itemString:
+ return tomlString
+ case itemMultilineString:
+ return tomlString
+ case itemRawString:
+ return tomlString
+ case itemRawMultilineString:
+ return tomlString
+ case itemBool:
+ return tomlBool
+ }
+ p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
+ panic("unreachable")
+}