Compare commits

...

32 Commits

Author SHA1 Message Date
Thomas Pelletier 8f6d0d8be7 Specialize map[string]iface when unmarshaling kvs
name                               old time/op    new time/op    delta
UnmarshalDataset/config-8            12.3ms ± 0%    10.9ms ± 0%  -11.36%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8            55.2ms ± 0%    55.4ms ± 1%     ~     (p=0.690 n=5+5)
UnmarshalDataset/citm_catalog-8      16.5ms ± 1%    15.7ms ± 0%   -4.43%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8           7.01ms ± 1%    6.63ms ± 0%   -5.34%  (p=0.008 n=5+5)
UnmarshalDataset/code-8              52.0ms ± 0%    48.7ms ± 2%   -6.43%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            119µs ± 0%     110µs ± 3%   -6.81%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8     432ns ± 1%     431ns ± 1%     ~     (p=0.222 n=5+5)
Unmarshal/SimpleDocument/map-8        573ns ± 1%     540ns ± 1%   -5.64%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8     33.7µs ± 0%    33.6µs ± 0%     ~     (p=0.310 n=5+5)
Unmarshal/ReferenceFile/map-8        44.2µs ± 1%    41.7µs ± 1%   -5.63%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8          7.28µs ± 1%    6.74µs ± 1%   -7.42%  (p=0.008 n=5+5)
[Geo mean]                            292µs          277µs        -4.91%

name                               old speed      new speed      delta
UnmarshalDataset/config-8          85.2MB/s ± 0%  96.1MB/s ± 0%  +12.82%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8          39.9MB/s ± 0%  39.8MB/s ± 1%     ~     (p=0.690 n=5+5)
UnmarshalDataset/citm_catalog-8    33.9MB/s ± 1%  35.5MB/s ± 0%   +4.64%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8         63.1MB/s ± 1%  66.6MB/s ± 0%   +5.65%  (p=0.008 n=5+5)
UnmarshalDataset/code-8            51.6MB/s ± 0%  55.1MB/s ± 2%   +6.88%  (p=0.008 n=5+5)
UnmarshalDataset/example-8         68.3MB/s ± 0%  73.4MB/s ± 3%   +7.34%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8  25.4MB/s ± 1%  25.5MB/s ± 1%     ~     (p=0.246 n=5+5)
Unmarshal/SimpleDocument/map-8     19.2MB/s ± 1%  20.4MB/s ± 1%   +5.99%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8    156MB/s ± 0%   156MB/s ± 0%     ~     (p=0.310 n=5+5)
Unmarshal/ReferenceFile/map-8       119MB/s ± 1%   126MB/s ± 1%   +5.97%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8        75.0MB/s ± 1%  81.0MB/s ± 1%   +8.01%  (p=0.008 n=5+5)
[Geo mean]                         56.1MB/s       59.0MB/s        +5.17%

name                               old alloc/op   new alloc/op   delta
UnmarshalDataset/config-8            5.26MB ± 0%    4.75MB ± 0%   -9.66%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8            83.0MB ± 0%    83.0MB ± 0%   -0.00%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8      34.7MB ± 0%    34.3MB ± 0%   -1.13%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8           12.7MB ± 0%    12.5MB ± 0%   -1.44%  (p=0.008 n=5+5)
UnmarshalDataset/code-8              15.3MB ± 0%    13.9MB ± 0%   -9.27%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            186kB ± 0%     182kB ± 0%   -2.20%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8      805B ± 0%      805B ± 0%     ~     (all equal)
Unmarshal/SimpleDocument/map-8       1.13kB ± 0%    1.12kB ± 0%   -1.41%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8     20.9kB ± 0%    20.9kB ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/map-8        36.4kB ± 0%    35.4kB ± 0%     ~     (p=0.079 n=4+5)
Unmarshal/HugoFrontMatter-8          7.20kB ± 0%    6.98kB ± 0%   -3.11%  (p=0.008 n=5+5)
[Geo mean]                            312kB          303kB        -2.86%

name                               old allocs/op  new allocs/op  delta
UnmarshalDataset/config-8              189k ± 0%      157k ± 0%  -16.80%  (p=0.029 n=4+4)
UnmarshalDataset/canada-8              782k ± 0%      782k ± 0%   -0.00%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8        191k ± 0%      167k ± 0%  -12.75%  (p=0.000 n=4+5)
UnmarshalDataset/twitter-8            56.9k ± 0%     45.5k ± 0%  -20.02%  (p=0.016 n=5+4)
UnmarshalDataset/code-8                626k ± 0%      537k ± 0%  -14.22%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            1.36k ± 0%     1.11k ± 0%  -18.53%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8      9.00 ± 0%      9.00 ± 0%     ~     (all equal)
Unmarshal/SimpleDocument/map-8         13.0 ± 0%      12.0 ± 0%   -7.69%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8        183 ± 0%       183 ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/map-8           526 ± 0%       466 ± 0%  -11.41%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8             126 ± 0%       112 ± 0%  -11.11%  (p=0.008 n=5+5)
[Geo mean]                            3.73k          3.34k       -10.51%
2022-04-15 17:41:18 -04:00
Thomas Pelletier 3c4b709fed Specialize navigating map[string]interface{}
This is a common type. Specializing it to reduce the use of reflection
yields better performance.

Similar to https://github.com/pelletier/go-toml/pull/669, there is a lot
to explore there.

name                               old time/op    new time/op    delta
UnmarshalDataset/config-8            13.3ms ± 0%    12.3ms ± 0%   -7.45%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8            55.7ms ± 0%    55.2ms ± 0%   -0.88%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8      16.6ms ± 1%    16.5ms ± 1%   -0.97%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8           7.10ms ± 1%    7.01ms ± 1%   -1.28%  (p=0.016 n=5+5)
UnmarshalDataset/code-8              63.8ms ± 0%    52.0ms ± 0%  -18.45%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            121µs ± 0%     119µs ± 0%   -2.13%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8     438ns ± 1%     432ns ± 1%   -1.40%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/map-8        594ns ± 2%     573ns ± 1%   -3.56%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8     34.3µs ± 1%    33.7µs ± 0%   -1.95%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/map-8        48.6µs ± 0%    44.2µs ± 1%   -9.22%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8          7.88µs ± 1%    7.28µs ± 1%   -7.66%  (p=0.008 n=5+5)

name                               old speed      new speed      delta
UnmarshalDataset/config-8          78.9MB/s ± 0%  85.2MB/s ± 0%   +8.05%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8          39.5MB/s ± 0%  39.9MB/s ± 0%   +0.89%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8    33.6MB/s ± 1%  33.9MB/s ± 1%   +0.98%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8         62.3MB/s ± 1%  63.1MB/s ± 1%   +1.30%  (p=0.016 n=5+5)
UnmarshalDataset/code-8            42.1MB/s ± 0%  51.6MB/s ± 0%  +22.62%  (p=0.008 n=5+5)
UnmarshalDataset/example-8         66.9MB/s ± 0%  68.3MB/s ± 0%   +2.18%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8  25.1MB/s ± 1%  25.4MB/s ± 1%   +1.43%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/map-8     18.5MB/s ± 2%  19.2MB/s ± 1%   +3.70%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-8    153MB/s ± 1%   156MB/s ± 0%   +1.99%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/map-8       108MB/s ± 0%   119MB/s ± 1%  +10.16%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8        69.3MB/s ± 1%  75.0MB/s ± 1%   +8.30%  (p=0.008 n=5+5)

name                               old alloc/op   new alloc/op   delta
UnmarshalDataset/config-8            5.86MB ± 0%    5.26MB ± 0%  -10.36%  (p=0.008 n=5+5)
UnmarshalDataset/canada-8            83.0MB ± 0%    83.0MB ± 0%   -0.00%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8      34.7MB ± 0%    34.7MB ± 0%   -0.04%  (p=0.008 n=5+5)
UnmarshalDataset/twitter-8           12.7MB ± 0%    12.7MB ± 0%     ~     (p=0.548 n=5+5)
UnmarshalDataset/code-8              22.2MB ± 0%    15.3MB ± 0%  -30.76%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            186kB ± 0%     186kB ± 0%   -0.04%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8      805B ± 0%      805B ± 0%     ~     (all equal)
Unmarshal/SimpleDocument/map-8       1.13kB ± 0%    1.13kB ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/struct-8     20.9kB ± 0%    20.9kB ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/map-8        38.2kB ± 0%    36.4kB ± 0%   -4.86%  (p=0.029 n=4+4)
Unmarshal/HugoFrontMatter-8          7.44kB ± 0%    7.20kB ± 0%   -3.23%  (p=0.008 n=5+5)

name                               old allocs/op  new allocs/op  delta
UnmarshalDataset/config-8              227k ± 0%      189k ± 0%  -16.74%  (p=0.029 n=4+4)
UnmarshalDataset/canada-8              782k ± 0%      782k ± 0%   -0.00%  (p=0.008 n=5+5)
UnmarshalDataset/citm_catalog-8        192k ± 0%      191k ± 0%   -0.49%  (p=0.000 n=5+4)
UnmarshalDataset/twitter-8            56.9k ± 0%     56.9k ± 0%   -0.00%  (p=0.032 n=5+5)
UnmarshalDataset/code-8               1.05M ± 0%     0.63M ± 0%  -40.52%  (p=0.008 n=5+5)
UnmarshalDataset/example-8            1.36k ± 0%     1.36k ± 0%   -0.15%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-8      9.00 ± 0%      9.00 ± 0%     ~     (all equal)
Unmarshal/SimpleDocument/map-8         13.0 ± 0%      13.0 ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/struct-8        183 ± 0%       183 ± 0%     ~     (all equal)
Unmarshal/ReferenceFile/map-8           642 ± 0%       526 ± 0%  -18.07%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-8             141 ± 0%       126 ± 0%  -10.64%  (p=0.008 n=5+5)
2022-04-15 17:20:24 -04:00
Thomas Pelletier adacebd8c7 Update benchmarks in README (#756) 2022-04-10 22:24:19 -04:00
Thomas Pelletier 8bbb673431 Fuzzing setup and fixes (#755)
* encode: fix localdate formatting
* encode: fix empty key marshaling
* encode: fix invalid quotation of time.Time
* encode: ensure control chars are escaped
* decode: always use UTC for zero tz
* encode: check for invalid characters in keys
* encode: always construct map for empty array tables
* fuzz: add go 1.18 fuzz test
* encode: handle NaNs
* encode: allow new lines in quoted keys
* encode: never emit table inside array
* encode: don't capitalize inf
2022-04-10 21:37:12 -04:00
Thomas Pelletier 2377ac4bc0 encode: fix embedded interfaces (#753)
Resolve marshaling regression when handling an embedded interface in a
struct.

Fixes #752
2022-04-08 09:25:54 -04:00
Thomas Pelletier f5cc8c49eb decoder: remove mention of UnmarshalText in errors (#751)
Fixes #737
2022-04-07 21:58:19 -04:00
Thomas Pelletier 89d7b412d8 decode: allow subtables to be defined later (#750)
Fixes #739
2022-04-07 21:49:16 -04:00
Thomas Pelletier 88a8aecdd4 tools: display error context when it exists (#749)
For example when failing to decode toml, display the context around the
error and the location of the problem.
2022-04-07 20:33:09 -04:00
Thomas Pelletier 9804fc57e0 decoder: support \e escape sequence (#748) 2022-04-07 20:18:30 -04:00
Thomas Pelletier 068279f13b encode: respect stdlib rules for embedded structs (#747) 2022-04-07 19:51:09 -04:00
Thomas Pelletier b9edbeb611 Update testify dependency (#746) 2022-03-30 15:10:57 -04:00
Thomas Pelletier a97c9317d4 Go 1.18 (#745) 2022-03-23 10:26:12 -04:00
Gregory Oschwald 3229a0abfb Decode: convert table key to correct type (#741)
Fixes #740.
2022-03-02 09:24:01 -05:00
Thomas Pelletier 3f5d8a6b06 Mention removal of go-toml/query (#736)
Fixes #722
2022-01-07 18:58:33 -05:00
Cameron Moore 146f70ea8a Decode: use cleaned byte slice throughout parseFloat (#735)
Fixes #734
2022-01-06 14:34:27 -05:00
Thomas Pelletier e83cf535f5 Decoder: rename SetStrict to DisallowUnknownFields (#731) 2022-01-02 14:32:34 -05:00
Thomas Pelletier c3ba3ef97a readme: add docker image 2022-01-01 09:53:14 -05:00
Thomas Pelletier 7ee3c8ff25 build: login to github repository 2022-01-01 09:24:52 -05:00
Thomas Pelletier 1e85aa6d78 build: add contents permissions 2021-12-31 20:47:50 -05:00
Thomas Pelletier 46fa3225e2 build: allows the token to write packages 2021-12-31 20:25:11 -05:00
Thomas Pelletier 4d51831dab build: replace grc.io with ghcr.io 2021-12-31 20:10:36 -05:00
Thomas Pelletier 5a1a96cb2d build: pass the github token to goreleaser 2021-12-31 20:03:18 -05:00
Thomas Pelletier ea9040ae83 build: change workflow reference to v2 2021-12-31 19:57:41 -05:00
Thomas Pelletier 2373685f1e Docker + goreleaser (#729) 2021-12-31 19:55:31 -05:00
Thomas Pelletier f1391952d4 Update and move testsuite to internal package (#730)
* Regenerate test suite

* Move test suite to /internal
2021-12-31 18:52:26 -05:00
Thomas Pelletier 4a73a200ed Clean up tools godoc 2021-12-31 15:56:40 -05:00
Thomas Pelletier 4807229e94 tomll: port to v2 (#727)
Fixes #721
2021-12-31 15:40:18 -05:00
Thomas Pelletier d8ddc00c61 jsontoml: port to v2 (#726)
Fixes #719
2021-12-31 14:40:20 -05:00
Thomas Pelletier 82f8dad811 tomljson: port to v2 (#725) 2021-12-31 13:25:53 -05:00
Thomas Pelletier 75db1016e8 Remove extra words from CONTRIBUTING 2021-12-29 10:26:22 -05:00
Thomas Pelletier de6d715bd2 Update CONTRIBUTING.md 2021-12-29 10:25:28 -05:00
Thomas Pelletier 3ab2fc2b87 Update release proces documentation 2021-12-29 10:24:01 -05:00
50 changed files with 1615 additions and 210 deletions
+1
View File
@@ -1,3 +1,4 @@
* text=auto * text=auto
benchmark/benchmark.toml text eol=lf benchmark/benchmark.toml text eol=lf
testdata/** text eol=lf
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
- name: Setup go - name: Setup go
uses: actions/setup-go@master uses: actions/setup-go@master
with: with:
go-version: 1.16 go-version: 1.18
- name: Run tests with coverage - name: Run tests with coverage
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}" run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
+39
View File
@@ -0,0 +1,39 @@
name: release
on:
push:
tags:
- "v2.*"
workflow_call:
inputs:
args:
description: "Extra arguments to pass goreleaser"
default: ""
required: false
type: string
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release ${{ inputs.args }} --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -1
View File
@@ -12,14 +12,21 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest'] os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
go: [ '1.16', '1.17' ] go: [ '1.17', '1.18' ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }} name: ${{ matrix.go }}/${{ matrix.os }}
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
with:
fetch-depth: 0
- name: Setup go ${{ matrix.go }} - name: Setup go ${{ matrix.go }}
uses: actions/setup-go@master uses: actions/setup-go@master
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run unit tests - name: Run unit tests
run: go test -race ./... run: go test -race ./...
release-check:
if: ${{ github.ref != 'refs/heads/v2' }}
uses: pelletier/go-toml/.github/workflows/release.yml@v2
with:
args: --snapshot
+1
View File
@@ -3,3 +3,4 @@ fuzz/
cmd/tomll/tomll cmd/tomll/tomll
cmd/tomljson/tomljson cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen cmd/tomltestgen/tomltestgen
dist
+111
View File
@@ -0,0 +1,111 @@
before:
hooks:
- go mod tidy
- go fmt ./...
- go test ./...
builds:
- id: tomll
main: ./cmd/tomll
binary: tomll
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- windows_amd64
- darwin_amd64
- darwin_arm64
- id: tomljson
main: ./cmd/tomljson
binary: tomljson
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- windows_amd64
- darwin_amd64
- darwin_arm64
- id: jsontoml
main: ./cmd/jsontoml
binary: jsontoml
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- windows_amd64
- darwin_amd64
- darwin_arm64
universal_binaries:
- id: tomll
replace: true
name_template: tomll
- id: tomljson
replace: true
name_template: tomljson
- id: jsontoml
replace: true
name_template: jsontoml
archives:
- id: jsontoml
format: tar.xz
builds:
- jsontoml
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomljson
format: tar.xz
builds:
- tomljson
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomll
format: tar.xz
builds:
- tomll
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
dockers:
- id: tools
goos: linux
goarch: amd64
ids:
- jsontoml
- tomljson
- tomll
image_templates:
- "ghcr.io/pelletier/go-toml:latest"
- "ghcr.io/pelletier/go-toml:{{ .Tag }}"
- "ghcr.io/pelletier/go-toml:v{{ .Major }}"
skip_push: false
checksum:
name_template: 'sha256sums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
release:
github:
owner: pelletier
name: go-toml
draft: true
prerelease: auto
mode: replace
changelog:
use: github-native
announce:
skip: true
+23 -9
View File
@@ -155,6 +155,8 @@ Checklist:
- Does not introduce backward-incompatible changes (unless discussed). - Does not introduce backward-incompatible changes (unless discussed).
- Has relevant doc changes. - Has relevant doc changes.
- Benchstat does not show performance regression. - Benchstat does not show performance regression.
- Pull request is [labeled appropriately][pr-labels].
- Title will be understandable in the changelog.
1. Merge using "squash and merge". 1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information 2. Make sure to edit the commit message to keep all the useful information
@@ -163,13 +165,25 @@ Checklist:
### New release ### New release
1. Go to [releases][releases]. Click on "X commits to master since this 1. Decide on the next version number. Use semver.
release". 2. Generate release notes using [`gh`][gh]. Example:
2. Make note of all the changes. Look for backward incompatible changes, ```
new features, and bug fixes. $ gh api -X POST \
3. Pick the new version using the above and semver. -F tag_name='v2.0.0-beta.5' \
4. Create a [new release][new-release]. -F target_commitish='v2' \
5. Follow the same format as [1.1.0][release-110]. -F previous_tag_name='v2.0.0-beta.4' \
--jq '.body' \
repos/pelletier/go-toml/releases/generate-notes
```
3. Look for "Other changes". That would indicate a pull request not labeled
properly. Tweak labels and pull request titles until changelog looks good for
users.
4. [Draft new release][new-release].
5. Fill tag and target with the same value used to generate the changelog.
6. Set title to the new tag value.
7. Paste the generated changelog.
8. Check "create discussion", in the "Releases" category.
9. Check pre-release if new version is an alpha or beta.
[issues-tracker]: https://github.com/pelletier/go-toml/issues [issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md [bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
@@ -177,6 +191,6 @@ Checklist:
[readme]: ./README.md [readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo [fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request [pull-request]: https://help.github.com/en/articles/creating-a-pull-request
[releases]: https://github.com/pelletier/go-toml/releases
[new-release]: https://github.com/pelletier/go-toml/releases/new [new-release]: https://github.com/pelletier/go-toml/releases/new
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0 [gh]: https://github.com/cli/cli
[pr-labels]: https://github.com/pelletier/go-toml/blob/v2/.github/release.yml
+5
View File
@@ -0,0 +1,5 @@
FROM scratch
ENV PATH "$PATH:/bin"
COPY tomll /bin/tomll
COPY tomljson /bin/tomljson
COPY jsontoml /bin/jsontoml
+73 -49
View File
@@ -21,7 +21,8 @@ encouraged to try out this version.
## Documentation ## Documentation
Full API, examples, and implementation notes are available in the Go documentation. Full API, examples, and implementation notes are available in the Go
documentation.
[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2) [![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
@@ -51,7 +52,7 @@ operations should not be shockingly slow. See [benchmarks](#benchmarks).
the TOML document was not prevent in the target structure. This is a great way the TOML document was not prevent in the target structure. This is a great way
to check for typos. [See example in the documentation][strict]. to check for typos. [See example in the documentation][strict].
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.SetStrict [strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.DisallowUnknownFields
### Contextualized errors ### Contextualized errors
@@ -160,11 +161,11 @@ Execution time speedup compared to other Go TOML libraries:
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>1.9x</td></tr> <tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>1.9x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>1.9x</td></tr> <tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>1.8x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.4x</td><td>2.6x</td></tr> <tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.5x</td></tr> <tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.7x</td><td>2.6x</td></tr> <tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.1x</td></tr> <tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.4x</td><td>5.3x</td></tr>
</tbody> </tbody>
</table> </table>
<details><summary>See more</summary> <details><summary>See more</summary>
@@ -177,17 +178,17 @@ provided for completeness.</p>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr> <tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.7x</td><td>2.1x</td></tr> <tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.9x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>2.8x</td></tr> <tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>4.2x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.1x</td><td>3.1x</td></tr> <tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.5x</td><td>3.1x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.4x</td><td>4.3x</td></tr> <tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.2x</td><td>3.9x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.4x</td><td>3.2x</td></tr> <tr><td>UnmarshalDataset/example-2</td><td>3.1x</td><td>3.5x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.2x</td><td>2.5x</td></tr> <tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>3.1x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.8x</td><td>2.7x</td></tr> <tr><td>UnmarshalDataset/twitter-2</td><td>2.5x</td><td>2.6x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.0x</td></tr> <tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.1x</td><td>2.2x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.4x</td></tr> <tr><td>UnmarshalDataset/canada-2</td><td>1.6x</td><td>1.3x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.4x</td><td>2.9x</td></tr> <tr><td>UnmarshalDataset/config-2</td><td>4.3x</td><td>3.2x</td></tr>
<tr><td>[Geo mean]</td><td>2.8x</td><td>2.6x</td></tr> <tr><td>[Geo mean]</td><td>2.7x</td><td>2.8x</td></tr>
</tbody> </tbody>
</table> </table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p> <p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
@@ -207,6 +208,44 @@ In case of trouble: [Go Modules FAQ][mod-faq].
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module [mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
## Tools
Go-toml provides three handy command line tools:
* `tomljson`: Reads a TOML file and outputs its JSON representation.
```
$ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
$ tomljson --help
```
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
```
$ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
$ jsontoml --help
```
* `tomll`: Lints and reformats a TOML file.
```
$ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
$ tomll --help
```
### Docker image
Those tools are also available as a [Docker image][docker]. For example, to use
`tomljson`:
```
docker run -i ghcr.io/pelletier/go-toml:v2 tomljson < example.toml
```
Multiple versions are availble on [ghcr.io][docker].
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
## Migrating from v1 ## Migrating from v1
This section describes the differences between v1 and v2, with some pointers on This section describes the differences between v1 and v2, with some pointers on
@@ -488,45 +527,30 @@ The new name is `Encoder.SetArraysMultiline`. The behavior should be the same.
The new name is `Encoder.SetIndentSymbol`. The behavior should be the same. The new name is `Encoder.SetIndentSymbol`. The behavior should be the same.
#### Embedded structs are tables #### Embedded structs behave like stdlib
V1 defaults to merging embedded struct fields into the embedding struct. This V1 defaults to merging embedded struct fields into the embedding struct. This
behavior was unexpected because it does not follow the standard library. To behavior was unexpected because it does not follow the standard library. To
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
added to make the encoder behave correctly. Given backward compatibility is not added to make the encoder behave correctly. Given backward compatibility is not
a problem anymore, v2 does the right thing by default. There is no way to revert a problem anymore, v2 does the right thing by default: it follows the behavior
to the old behavior, and `Encoder.PromoteAnonymous` has been removed. of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.
```go
type Embedded struct {
Value string `toml:"value"`
}
type Doc struct {
Embedded
}
d := Doc{}
fmt.Println("v1:")
b, err := v1.Marshal(d)
fmt.Println(string(b))
fmt.Println("v2:")
b, err = v2.Marshal(d)
fmt.Println(string(b))
// Output:
// v1:
// value = ""
//
// v2:
// [Embedded]
// value = ''
```
[nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038 [nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038
### `query`
go-toml v1 provided the [`go-toml/query`][query] package. It allowed to run
JSONPath-style queries on TOML files. This feature is not available in v2. For a
replacement, check out [dasel][dasel].
This package has been removed because it was essentially not supported anymore
(last commit May 2020), increased the complexity of the code base, and more
complete solutions exist out there.
[query]: https://github.com/pelletier/go-toml/tree/f99d6bbca119636aeafcf351ee52b3d202782627/query
[dasel]: https://github.com/TomWright/dasel
## License ## License
The MIT License (MIT). Read [LICENSE](LICENSE). The MIT License (MIT). Read [LICENSE](LICENSE).
+13 -5
View File
@@ -76,7 +76,8 @@ cover() {
fi fi
pushd "$dir" pushd "$dir"
go test -covermode=atomic -coverprofile=coverage.out ./... go test -covermode=atomic -coverpkg=./... -coverprofile=coverage.out.tmp ./...
cat coverage.out.tmp | grep -v testsuite | grep -v tomltestgen | grep -v gotoml-test-decoder > coverage.out
go tool cover -func=coverage.out go tool cover -func=coverage.out
popd popd
@@ -103,16 +104,23 @@ coverage() {
echo "" echo ""
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')" target_pct="$(tail -n2 ${target_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%.*/\1/')"
head_pct="$(cat ${head_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')" head_pct="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')"
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%" echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
delta_pct=$(echo "$head_pct - $target_pct" | bc -l) delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
echo "Delta: ${delta_pct}" echo "Delta: ${delta_pct}"
if [[ $delta_pct = \-* ]]; then if [[ $delta_pct = \-* ]]; then
echo "Regression!"; echo "Regression!";
return 1
target_diff="${output_dir}/target.diff.txt"
head_diff="${output_dir}/head.diff.txt"
cat "${target_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${target_diff}"
cat "${head_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${head_diff}"
diff --side-by-side --suppress-common-lines "${target_diff}" "${head_diff}"
return 1
fi fi
return 0 return 0
;; ;;
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"os" "os"
"path" "path"
"github.com/pelletier/go-toml/v2/testsuite" "github.com/pelletier/go-toml/v2/internal/testsuite"
) )
func main() { func main() {
+55
View File
@@ -0,0 +1,55 @@
// Package jsontoml is a program that converts JSON to TOML.
//
// Usage
//
// Reading from stdin:
//
// cat file.json | jsontoml > file.toml
//
// Reading from a file:
//
// jsontoml file.json > file.toml
//
// Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
package main
import (
"encoding/json"
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `jsontoml can be used in two ways:
Reading from stdin:
cat file.json | jsontoml > file.toml
Reading from a file:
jsontoml file.json > file.toml
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := json.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
return err
}
e := toml.NewEncoder(w)
return e.Encode(v)
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvert(t *testing.T) {
examples := []struct {
name string
input string
expected string
errors bool
}{
{
name: "valid json",
input: `
{
"mytoml": {
"a": 42
}
}`,
expected: `[mytoml]
a = 42.0
`,
},
{
name: "invalid json",
input: `{ foo`,
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
err := convert(strings.NewReader(e.input), b)
if e.errors {
require.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, e.expected, b.String())
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Package tomljson is a program that converts TOML to JSON.
//
// Usage
//
// Reading from stdin:
//
// cat file.toml | tomljson > file.json
//
// Reading from a file:
//
// tomljson file.toml > file.json
//
// Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `tomljson can be used in two ways:
Reading from stdin:
cat file.toml | tomljson > file.json
Reading from a file:
tomljson file.toml > file.json
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := toml.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
var derr *toml.DecodeError
if errors.As(err, &derr) {
row, col := derr.Position()
return fmt.Errorf("%s\nerror occurred at row %d column %d", derr.String(), row, col)
}
return err
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
return e.Encode(v)
}
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvert(t *testing.T) {
examples := []struct {
name string
input io.Reader
expected string
errors bool
}{
{
name: "valid toml",
input: strings.NewReader(`
[mytoml]
a = 42`),
expected: `{
"mytoml": {
"a": 42
}
}
`,
},
{
name: "invalid toml",
input: strings.NewReader(`bad = []]`),
errors: true,
},
{
name: "bad reader",
input: &badReader{},
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
err := convert(e.input, b)
if e.errors {
require.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, e.expected, b.String())
}
}
}
type badReader struct{}
func (r *badReader) Read([]byte) (int, error) {
return 0, fmt.Errorf("reader failed on purpose")
}
+58
View File
@@ -0,0 +1,58 @@
// Package tomll is a linter program for TOML.
//
// Usage
//
// Reading from stdin, writing to stdout:
//
// cat file.toml | tomll
//
// Reading and updating a list of files in place:
//
// tomll a.toml b.toml c.toml
//
// Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
package main
import (
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `tomll can be used in two ways:
Reading from stdin, writing to stdout:
cat file.toml | tomll > file.toml
Reading and updating a list of files in place:
tomll a.toml b.toml c.toml
When given a list of files, tomll will modify all files in place without asking.
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
Inplace: true,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := toml.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
return err
}
e := toml.NewEncoder(w)
return e.Encode(v)
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvert(t *testing.T) {
examples := []struct {
name string
input string
expected string
errors bool
}{
{
name: "valid toml",
input: `
mytoml.a = 42.0
`,
expected: `[mytoml]
a = 42.0
`,
},
{
name: "invalid toml",
input: `[what`,
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
err := convert(strings.NewReader(e.input), b)
if e.errors {
require.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, e.expected, b.String())
}
}
}
+7 -3
View File
@@ -130,7 +130,11 @@ func parseDateTime(b []byte) (time.Time, error) {
} }
seconds := direction * (hours*3600 + minutes*60) seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds) if seconds == 0 {
zone = time.UTC
} else {
zone = time.FixedZone("", seconds)
}
b = b[dateTimeByteLen:] b = b[dateTimeByteLen:]
} }
@@ -309,10 +313,10 @@ func parseFloat(b []byte) (float64, error) {
} }
start := 0 start := 0
if b[0] == '+' || b[0] == '-' { if cleaned[0] == '+' || cleaned[0] == '-' {
start = 1 start = 1
} }
if b[start] == '0' && isDigit(b[start+1]) { if cleaned[start] == '0' && isDigit(cleaned[start+1]) {
return 0, newDecodeError(b, "float integer part cannot have leading zeroes") return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
} }
+1 -1
View File
@@ -27,7 +27,7 @@ type DecodeError struct {
// corresponding field in the target value. It contains all the missing fields // corresponding field in the target value. It contains all the missing fields
// in Errors. // in Errors.
// //
// Emitted by Decoder when SetStrict(true) was called. // Emitted by Decoder when DisallowUnknownFields() was called.
type StrictMissingError struct { type StrictMissingError struct {
// One error per field that could not be found. // One error per field that could not be found.
Errors []DecodeError Errors []DecodeError
+56
View File
@@ -0,0 +1,56 @@
//go:build go1.18
// +build go1.18
package toml_test
import (
"io/ioutil"
"strings"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func FuzzUnmarshal(f *testing.F) {
file, err := ioutil.ReadFile("benchmark/benchmark.toml")
if err != nil {
panic(err)
}
f.Add(file)
f.Fuzz(func(t *testing.T, b []byte) {
if strings.Contains(string(b), "nan") {
// Current limitation of testify.
// https://github.com/stretchr/testify/issues/624
t.Skip("can't compare NaNs")
}
t.Log("INITIAL DOCUMENT ===========================")
t.Log(string(b))
var v interface{}
err := toml.Unmarshal(b, &v)
if err != nil {
return
}
t.Log("DECODED VALUE ===========================")
t.Logf("%#+v", v)
encoded, err := toml.Marshal(v)
if err != nil {
t.Fatalf("cannot marshal unmarshaled document: %s", err)
}
t.Log("ENCODED DOCUMENT ===========================")
t.Log(string(encoded))
var v2 interface{}
err = toml.Unmarshal(encoded, &v2)
if err != nil {
t.Fatalf("failed round trip: %s", err)
}
require.Equal(t, v, v2)
})
}
+1 -2
View File
@@ -2,5 +2,4 @@ module github.com/pelletier/go-toml/v2
go 1.16 go 1.16
// latest (v1.7.0) doesn't have the fix for time.Time require github.com/stretchr/testify v1.7.1
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
+2 -2
View File
@@ -3,8 +3,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+88
View File
@@ -0,0 +1,88 @@
package cli
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml/v2"
)
type ConvertFn func(r io.Reader, w io.Writer) error
type Program struct {
Usage string
Fn ConvertFn
// Inplace allows the command to take more than one file as argument and
// perform convertion in place on each provided file.
Inplace bool
}
func (p *Program) Execute() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, p.Usage) }
flag.Parse()
os.Exit(p.main(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func (p *Program) main(files []string, input io.Reader, output, error io.Writer) int {
err := p.run(files, input, output)
if err != nil {
var derr *toml.DecodeError
if errors.As(err, &derr) {
fmt.Fprintln(error, derr.String())
row, col := derr.Position()
fmt.Fprintln(error, "error occurred at row", row, "column", col)
} else {
fmt.Fprintln(error, err.Error())
}
return -1
}
return 0
}
func (p *Program) run(files []string, input io.Reader, output io.Writer) error {
if len(files) > 0 {
if p.Inplace {
return p.runAllFilesInPlace(files)
}
f, err := os.Open(files[0])
if err != nil {
return err
}
defer f.Close()
input = f
}
return p.Fn(input, output)
}
func (p *Program) runAllFilesInPlace(files []string) error {
for _, path := range files {
err := p.runFileInPlace(path)
if err != nil {
return err
}
}
return nil
}
func (p *Program) runFileInPlace(path string) error {
in, err := ioutil.ReadFile(path)
if err != nil {
return err
}
out := new(bytes.Buffer)
err = p.Fn(bytes.NewReader(in), out)
if err != nil {
return err
}
return ioutil.WriteFile(path, out.Bytes(), 0600)
}
+172
View File
@@ -0,0 +1,172 @@
package cli
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func processMain(args []string, input io.Reader, stdout, stderr io.Writer, f ConvertFn) int {
p := Program{Fn: f}
return p.main(args, input, stdout, stderr)
}
func TestProcessMainStdin(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, 0, exit)
assert.Empty(t, stdout.String())
assert.Empty(t, stderr.String())
}
func TestProcessMainStdinErr(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
return fmt.Errorf("something bad")
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.NotEmpty(t, stderr.String())
}
func TestProcessMainStdinDecodeErr(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
var v interface{}
return toml.Unmarshal([]byte(`qwe = 001`), &v)
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.Contains(t, stderr.String(), "error occurred at")
}
func TestProcessMainFileExists(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "example")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = tmpfile.Write([]byte(`some data`))
require.NoError(t, err)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
exit := processMain([]string{tmpfile.Name()}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, 0, exit)
assert.Empty(t, stdout.String())
assert.Empty(t, stderr.String())
}
func TestProcessMainFileDoesNotExist(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
exit := processMain([]string{"/lets/hope/this/does/not/exist"}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.NotEmpty(t, stderr.String())
}
func TestProcessMainFilesInPlace(t *testing.T) {
dir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
path2 := path.Join(dir, "file2")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
require.NoError(t, err)
err = ioutil.WriteFile(path2, []byte("content 2"), 0600)
require.NoError(t, err)
p := Program{
Fn: dummyFileFn,
Inplace: true,
}
exit := p.main([]string{path1, path2}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, 0, exit)
v1, err := ioutil.ReadFile(path1)
require.NoError(t, err)
require.Equal(t, "1", string(v1))
v2, err := ioutil.ReadFile(path2)
require.NoError(t, err)
require.Equal(t, "2", string(v2))
}
func TestProcessMainFilesInPlaceErrRead(t *testing.T) {
p := Program{
Fn: dummyFileFn,
Inplace: true,
}
exit := p.main([]string{"/this/path/is/invalid"}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, -1, exit)
}
func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
dir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
require.NoError(t, err)
p := Program{
Fn: func(io.Reader, io.Writer) error { return fmt.Errorf("oh no") },
Inplace: true,
}
exit := p.main([]string{path1}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, -1, exit)
v1, err := ioutil.ReadFile(path1)
require.NoError(t, err)
require.Equal(t, "content 1", string(v1))
}
func dummyFileFn(r io.Reader, w io.Writer) error {
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
v := strings.SplitN(string(b), " ", 2)[1]
_, err = w.Write([]byte(v))
return err
}
@@ -927,6 +927,29 @@ func TestUnmarshalMapWithTypedKey(t *testing.T) {
} }
} }
func TestUnmarshalTypeTableHeader(t *testing.T) {
testToml := []byte(`
[test]
a = 1
`)
type header string
var result map[header]map[string]int
err := toml.Unmarshal(testToml, &result)
if err != nil {
t.Errorf("Received unexpected error: %s", err)
return
}
expected := map[header]map[string]int{
"test": map[string]int{"a": 1},
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad unmarshal: expected %v, got %v", expected, result)
}
}
func TestUnmarshalNonPointer(t *testing.T) { func TestUnmarshalNonPointer(t *testing.T) {
a := 1 a := 1
err := toml.Unmarshal([]byte{}, a) err := toml.Unmarshal([]byte{}, a)
@@ -1925,7 +1948,7 @@ func decoder(doc string) *toml.Decoder {
func strictDecoder(doc string) *toml.Decoder { func strictDecoder(doc string) *toml.Decoder {
d := decoder(doc) d := decoder(doc)
d.SetStrict(true) d.DisallowUnknownFields()
return d return d
} }
+12 -7
View File
@@ -79,6 +79,7 @@ type entry struct {
name []byte name []byte
kind keyKind kind keyKind
explicit bool explicit bool
kv bool
} }
// Find the index of the child of parentIdx with key k. Returns -1 if // Find the index of the child of parentIdx with key k. Returns -1 if
@@ -111,7 +112,7 @@ func (s *SeenTracker) clear(idx int) {
s.entries[idx].child = -1 s.entries[idx].child = -1
} }
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool) int { func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool, kv bool) int {
e := entry{ e := entry{
child: -1, child: -1,
next: s.entries[parentIdx].child, next: s.entries[parentIdx].child,
@@ -119,6 +120,7 @@ func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit
name: name, name: name,
kind: kind, kind: kind,
explicit: explicit, explicit: explicit,
kv: kv,
} }
var idx int var idx int
if s.entries[0].next >= 0 { if s.entries[0].next >= 0 {
@@ -137,7 +139,10 @@ func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit
func (s *SeenTracker) setExplicitFlag(parentIdx int) { func (s *SeenTracker) setExplicitFlag(parentIdx int) {
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next { for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
s.entries[i].explicit = true if s.entries[i].kv {
s.entries[i].explicit = true
s.entries[i].kv = false
}
s.setExplicitFlag(i) s.setExplicitFlag(i)
} }
} }
@@ -183,7 +188,7 @@ func (s *SeenTracker) checkTable(node *ast.Node) error {
idx := s.find(parentIdx, k) idx := s.find(parentIdx, k)
if idx < 0 { if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false) idx = s.create(parentIdx, k, tableKind, false, false)
} else { } else {
entry := s.entries[idx] entry := s.entries[idx]
if entry.kind == valueKind { if entry.kind == valueKind {
@@ -206,7 +211,7 @@ func (s *SeenTracker) checkTable(node *ast.Node) error {
} }
s.entries[idx].explicit = true s.entries[idx].explicit = true
} else { } else {
idx = s.create(parentIdx, k, tableKind, true) idx = s.create(parentIdx, k, tableKind, true, false)
} }
s.currentIdx = idx s.currentIdx = idx
@@ -233,7 +238,7 @@ func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
idx := s.find(parentIdx, k) idx := s.find(parentIdx, k)
if idx < 0 { if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false) idx = s.create(parentIdx, k, tableKind, false, false)
} else { } else {
entry := s.entries[idx] entry := s.entries[idx]
if entry.kind == valueKind { if entry.kind == valueKind {
@@ -254,7 +259,7 @@ func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
} }
s.clear(idx) s.clear(idx)
} else { } else {
idx = s.create(parentIdx, k, arrayTableKind, true) idx = s.create(parentIdx, k, arrayTableKind, true, false)
} }
s.currentIdx = idx s.currentIdx = idx
@@ -272,7 +277,7 @@ func (s *SeenTracker) checkKeyValue(node *ast.Node) error {
idx := s.find(parentIdx, k) idx := s.find(parentIdx, k)
if idx < 0 { if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false) idx = s.create(parentIdx, k, tableKind, false, true)
} else { } else {
entry := s.entries[idx] entry := s.entries[idx]
if it.IsLast() { if it.IsLast() {
+15
View File
@@ -0,0 +1,15 @@
package tracker
import (
"testing"
"unsafe"
"github.com/stretchr/testify/require"
)
func TestEntrySize(t *testing.T) {
// Validate no regression on the size of entry{}. This is a critical bit for
// performance of unmarshaling documents. Should only be increased with care
// and a very good reason.
require.LessOrEqual(t, 48, int(unsafe.Sizeof(entry{})))
}
+94 -54
View File
@@ -208,11 +208,20 @@ func (ctx *encoderCtx) isRoot() bool {
} }
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if !v.IsZero() { i := v.Interface()
i, ok := v.Interface().(time.Time)
if ok { switch x := i.(type) {
return i.AppendFormat(b, time.RFC3339), nil case time.Time:
if x.Nanosecond() > 0 {
return x.AppendFormat(b, time.RFC3339Nano), nil
} }
return x.AppendFormat(b, time.RFC3339), nil
case LocalTime:
return append(b, x.String()...), nil
case LocalDate:
return append(b, x.String()...), nil
case LocalDateTime:
return append(b, x.String()...), nil
} }
hasTextMarshaler := v.Type().Implements(textMarshalerType) hasTextMarshaler := v.Type().Implements(textMarshalerType)
@@ -260,16 +269,31 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.String: case reflect.String:
b = enc.encodeString(b, v.String(), ctx.options) b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32: case reflect.Float32:
if math.Trunc(v.Float()) == v.Float() { f := v.Float()
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 32)
if math.IsNaN(f) {
b = append(b, "nan"...)
} else if f > math.MaxFloat32 {
b = append(b, "inf"...)
} else if f < -math.MaxFloat32 {
b = append(b, "-inf"...)
} else if math.Trunc(f) == f {
b = strconv.AppendFloat(b, f, 'f', 1, 32)
} else { } else {
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32) b = strconv.AppendFloat(b, f, 'f', -1, 32)
} }
case reflect.Float64: case reflect.Float64:
if math.Trunc(v.Float()) == v.Float() { f := v.Float()
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 64) if math.IsNaN(f) {
b = append(b, "nan"...)
} else if f > math.MaxFloat64 {
b = append(b, "inf"...)
} else if f < -math.MaxFloat64 {
b = append(b, "-inf"...)
} else if math.Trunc(f) == f {
b = strconv.AppendFloat(b, f, 'f', 1, 64)
} else { } else {
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64) b = strconv.AppendFloat(b, f, 'f', -1, 64)
} }
case reflect.Bool: case reflect.Bool:
if v.Bool() { if v.Bool() {
@@ -300,10 +324,6 @@ func isNil(v reflect.Value) bool {
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error var err error
if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context")
}
if (ctx.options.omitempty || options.omitempty) && isEmptyValue(v) { if (ctx.options.omitempty || options.omitempty) && isEmptyValue(v) {
return b, nil return b, nil
} }
@@ -313,12 +333,7 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
} }
b = enc.indent(ctx.indent, b) b = enc.indent(ctx.indent, b)
b = enc.encodeKey(b, ctx.key)
b, err = enc.encodeKey(b, ctx.key)
if err != nil {
return nil, err
}
b = append(b, " = "...) b = append(b, " = "...)
// create a copy of the context because the value of a KV shouldn't // create a copy of the context because the value of a KV shouldn't
@@ -365,7 +380,13 @@ func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byt
} }
func needsQuoting(v string) bool { func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t") // TODO: vectorize
for _, b := range []byte(v) {
if b == '\'' || b == '\r' || b == '\n' || invalidAscii(b) {
return true
}
}
return false
} }
// caller should have checked that the string does not contain new lines or ' . // caller should have checked that the string does not contain new lines or ' .
@@ -437,7 +458,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
return b return b
} }
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ . // caller should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte { func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...) return append(b, v...)
} }
@@ -453,20 +474,11 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
b = append(b, '[') b = append(b, '[')
var err error b = enc.encodeKey(b, ctx.parentKey[0])
b, err = enc.encodeKey(b, ctx.parentKey[0])
if err != nil {
return nil, err
}
for _, k := range ctx.parentKey[1:] { for _, k := range ctx.parentKey[1:] {
b = append(b, '.') b = append(b, '.')
b = enc.encodeKey(b, k)
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
}
} }
b = append(b, "]\n"...) b = append(b, "]\n"...)
@@ -475,19 +487,19 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
} }
//nolint:cyclop //nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) { func (enc *Encoder) encodeKey(b []byte, k string) []byte {
needsQuotation := false needsQuotation := false
cannotUseLiteral := false cannotUseLiteral := false
if len(k) == 0 {
return append(b, "''"...)
}
for _, c := range k { for _, c := range k {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue continue
} }
if c == '\n' {
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
}
if c == literalQuote { if c == literalQuote {
cannotUseLiteral = true cannotUseLiteral = true
} }
@@ -495,13 +507,17 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation = true needsQuotation = true
} }
if needsQuotation && needsQuoting(k) {
cannotUseLiteral = true
}
switch { switch {
case cannotUseLiteral: case cannotUseLiteral:
return enc.encodeQuotedString(false, b, k), nil return enc.encodeQuotedString(false, b, k)
case needsQuotation: case needsQuotation:
return enc.encodeLiteralString(b, k), nil return enc.encodeLiteralString(b, k)
default: default:
return enc.encodeUnquotedKey(b, k), nil return enc.encodeUnquotedKey(b, k)
} }
} }
@@ -555,16 +571,25 @@ type table struct {
} }
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) { func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
for _, e := range t.kvs {
if e.Key == k {
return
}
}
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options}) t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
} }
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) { func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
for _, e := range t.tables {
if e.Key == k {
return
}
}
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options}) t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
} }
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
var t table
// TODO: cache this // TODO: cache this
typ := v.Type() typ := v.Type()
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
@@ -575,8 +600,6 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
k := fieldType.Name
tag := fieldType.Tag.Get("toml") tag := fieldType.Tag.Get("toml")
// special field name to skip field // special field name to skip field
@@ -584,13 +607,24 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
name, opts := parseTag(tag) k, opts := parseTag(tag)
if isValidName(name) { if !isValidName(k) {
k = name k = ""
} }
f := v.Field(i) f := v.Field(i)
if k == "" {
if fieldType.Anonymous {
if fieldType.Type.Kind() == reflect.Struct {
walkStruct(ctx, t, f)
}
continue
} else {
k = fieldType.Name
}
}
if isNil(f) { if isNil(f) {
continue continue
} }
@@ -607,6 +641,12 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
t.pushTable(k, f, options) t.pushTable(k, f, options)
} }
} }
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table
walkStruct(ctx, &t, v)
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
@@ -779,6 +819,9 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
} }
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool { func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
if ctx.insideKv {
return false
}
t := v.Type() t := v.Type()
if t.Kind() == reflect.Interface { if t.Kind() == reflect.Interface {
@@ -824,7 +867,6 @@ func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]by
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
ctx.shiftKey() ctx.shiftKey()
var err error
scratch := make([]byte, 0, 64) scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...) scratch = append(scratch, "[["...)
@@ -833,10 +875,7 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
scratch = append(scratch, '.') scratch = append(scratch, '.')
} }
scratch, err = enc.encodeKey(scratch, k) scratch = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
} }
scratch = append(scratch, "]]\n"...) scratch = append(scratch, "]]\n"...)
@@ -845,6 +884,7 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
b = append(b, scratch...) b = append(b, scratch...)
var err error
b, err = enc.encode(b, ctx, v.Index(i)) b, err = enc.encode(b, ctx, v.Index(i))
if err != nil { if err != nil {
return nil, err return nil, err
+145 -6
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"math/big" "math/big"
"strings" "strings"
"testing" "testing"
@@ -45,7 +46,7 @@ func TestMarshal(t *testing.T) {
v: map[string]string{ v: map[string]string{
"hel\nlo": "world", "hel\nlo": "world",
}, },
err: true, expected: `"hel\nlo" = 'world'`,
}, },
{ {
desc: `map with " in key`, desc: `map with " in key`,
@@ -380,7 +381,8 @@ hello = 'world'`,
v: map[string][]map[string]string{ v: map[string][]map[string]string{
"a\n": {{"hello": "world"}}, "a\n": {{"hello": "world"}},
}, },
err: true, expected: `[["a\n"]]
hello = 'world'`,
}, },
{ {
desc: "newline in map in slice", desc: "newline in map in slice",
@@ -440,7 +442,7 @@ hello = 'world'`,
v: map[string]interface{}{ v: map[string]interface{}{
"hello\nworld": 42, "hello\nworld": 42,
}, },
err: true, expected: `"hello\nworld" = 42`,
}, },
{ {
desc: "new line in parent of nested table key", desc: "new line in parent of nested table key",
@@ -449,7 +451,8 @@ hello = 'world'`,
"inner": 42, "inner": 42,
}, },
}, },
err: true, expected: `["hello\nworld"]
inner = 42`,
}, },
{ {
desc: "new line in nested table key", desc: "new line in nested table key",
@@ -460,7 +463,9 @@ hello = 'world'`,
}, },
}, },
}, },
err: true, expected: `[parent]
[parent."in\ner"]
foo = 42`,
}, },
{ {
desc: "invalid map key", desc: "invalid map key",
@@ -483,7 +488,16 @@ hello = 'world'`,
}{ }{
T: time.Time{}, T: time.Time{},
}, },
expected: `T = '0001-01-01T00:00:00Z'`, expected: `T = 0001-01-01T00:00:00Z`,
},
{
desc: "time nano",
v: struct {
T time.Time
}{
T: time.Date(1979, time.May, 27, 0, 32, 0, 999999000, time.UTC),
},
expected: `T = 1979-05-27T00:32:00.999999Z`,
}, },
{ {
desc: "bool", desc: "bool",
@@ -656,6 +670,33 @@ func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset)) assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset))
} }
func TestMarshalFloats(t *testing.T) {
v := map[string]float32{
"nan": float32(math.NaN()),
"+inf": float32(math.Inf(1)),
"-inf": float32(math.Inf(-1)),
}
expected := `'+inf' = inf
-inf = -inf
nan = nan
`
actual, err := toml.Marshal(v)
require.NoError(t, err)
require.Equal(t, expected, string(actual))
v64 := map[string]float64{
"nan": math.NaN(),
"+inf": math.Inf(1),
"-inf": math.Inf(-1),
}
actual, err = toml.Marshal(v64)
require.NoError(t, err)
require.Equal(t, expected, string(actual))
}
//nolint:funlen //nolint:funlen
func TestMarshalIndentTables(t *testing.T) { func TestMarshalIndentTables(t *testing.T) {
examples := []struct { examples := []struct {
@@ -947,6 +988,104 @@ func TestIssue678(t *testing.T) {
require.Equal(t, cfg, cfg2) require.Equal(t, cfg, cfg2)
} }
func TestIssue752(t *testing.T) {
type Fooer interface {
Foo() string
}
type Container struct {
Fooer
}
c := Container{}
out, err := toml.Marshal(c)
require.NoError(t, err)
require.Equal(t, "", string(out))
}
func TestMarshalNestedAnonymousStructs(t *testing.T) {
type Embedded struct {
Value string `toml:"value" json:"value"`
Top struct {
Value string `toml:"value" json:"value"`
} `toml:"top" json:"top"`
}
type Named struct {
Value string `toml:"value" json:"value"`
}
var doc struct {
Embedded
Named `toml:"named" json:"named"`
Anonymous struct {
Value string `toml:"value" json:"value"`
} `toml:"anonymous" json:"anonymous"`
}
expected := `value = ''
[top]
value = ''
[named]
value = ''
[anonymous]
value = ''
`
result, err := toml.Marshal(doc)
require.NoError(t, err)
require.Equal(t, expected, string(result))
}
func TestMarshalNestedAnonymousStructs_DuplicateField(t *testing.T) {
type Embedded struct {
Value string `toml:"value" json:"value"`
Top struct {
Value string `toml:"value" json:"value"`
} `toml:"top" json:"top"`
}
var doc struct {
Value string `toml:"value" json:"value"`
Embedded
}
doc.Embedded.Value = "shadowed"
doc.Value = "shadows"
expected := `value = 'shadows'
[top]
value = ''
`
result, err := toml.Marshal(doc)
require.NoError(t, err)
require.NoError(t, err)
require.Equal(t, expected, string(result))
}
func TestLocalTime(t *testing.T) {
v := map[string]toml.LocalTime{
"a": toml.LocalTime{
Hour: 1,
Minute: 2,
Second: 3,
Nanosecond: 4,
},
}
expected := `a = 01:02:03.000000004
`
out, err := toml.Marshal(v)
require.NoError(t, err)
require.Equal(t, expected, string(out))
}
func ExampleMarshal() { func ExampleMarshal() {
type MyConfig struct { type MyConfig struct {
Version int Version int
+4
View File
@@ -617,6 +617,8 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
builder.WriteByte('\r') builder.WriteByte('\r')
case 't': case 't':
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u': case 'u':
x, err := hexToRune(atmost(token[i+1:], 4), 4) x, err := hexToRune(atmost(token[i+1:], 4), 4)
if err != nil { if err != nil {
@@ -774,6 +776,8 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteByte('\r') builder.WriteByte('\r')
case 't': case 't':
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u': case 'u':
x, err := hexToRune(token[i+1:len(token)-1], 4) x, err := hexToRune(token[i+1:len(token)-1], 4)
if err != nil { if err != nil {
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=0000-01-01 00:00:00")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"\\n\"=\"\"")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("''=0")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=0000-01-01")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=\"\"\"\\U00000000\"\"\"")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=[[{}]]")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"\\b\"=\"\"")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=inf")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=0000-01-01 00:00:00+00:00")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=[{}]")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0=nan")
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing" "testing"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/testsuite" "github.com/pelletier/go-toml/v2/internal/testsuite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
+215 -32
View File
@@ -1,4 +1,4 @@
// Generated by tomltestgen for toml-test ref master on 2021-11-08T22:33:24-05:00 // Generated by tomltestgen for toml-test ref master on 2022-04-07T20:09:42-04:00
package toml_test package toml_test
import ( import (
@@ -55,11 +55,51 @@ func TestTOMLTest_Invalid_Array_TextInArray(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Bool_AlmostFalseWithExtra(t *testing.T) {
input := "a = falsify\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_AlmostFalse(t *testing.T) {
input := "a = fals\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_AlmostTrueWithExtra(t *testing.T) {
input := "a = truthy\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_AlmostTrue(t *testing.T) {
input := "a = tru\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_JustF(t *testing.T) {
input := "a = f\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_JustT(t *testing.T) {
input := "a = t\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_MixedCase(t *testing.T) { func TestTOMLTest_Invalid_Bool_MixedCase(t *testing.T) {
input := "valid = False\n" input := "valid = False\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Bool_StartingSameFalse(t *testing.T) {
input := "a = falsey\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_StartingSameTrue(t *testing.T) {
input := "a = truer\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Bool_WrongCaseFalse(t *testing.T) { func TestTOMLTest_Invalid_Bool_WrongCaseFalse(t *testing.T) {
input := "b = FALSE\n" input := "b = FALSE\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -70,6 +110,31 @@ func TestTOMLTest_Invalid_Bool_WrongCaseTrue(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Control_BareCr(t *testing.T) {
input := "# The following line contains a single carriage return control character\r\n\r"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Control_BareFormfeed(t *testing.T) {
input := "bare-formfeed = \f\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Control_BareNull(t *testing.T) {
input := "bare-null = \"some value\" \x00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Control_BareVerticalTab(t *testing.T) {
input := "bare-vertical-tab = \v\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Control_CommentCr(t *testing.T) {
input := "comment-cr = \"Carriage return in comment\" # \ra=1\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Control_CommentDel(t *testing.T) { func TestTOMLTest_Invalid_Control_CommentDel(t *testing.T) {
input := "comment-del = \"0x7f\" # \u007f\n" input := "comment-del = \"0x7f\" # \u007f\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -175,33 +240,73 @@ func TestTOMLTest_Invalid_Control_StringUs(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_ImpossibleDate(t *testing.T) { func TestTOMLTest_Invalid_Datetime_HourOver(t *testing.T) {
input := "d = 2006-01-50T00:00:00Z\n" input := "# time-hour = 2DIGIT ; 00-23\nd = 2006-01-01T24:00:00-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_MdayOver(t *testing.T) {
input := "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-32T00:00:00-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_MdayUnder(t *testing.T) {
input := "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-00T00:00:00-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_MinuteOver(t *testing.T) {
input := "# time-minute = 2DIGIT ; 00-59\nd = 2006-01-01T00:60:00-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_MonthOver(t *testing.T) {
input := "# date-month = 2DIGIT ; 01-12\nd = 2006-13-01T00:00:00-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_MonthUnder(t *testing.T) {
input := "# date-month = 2DIGIT ; 01-12\nd = 2007-00-01T00:00:00-00:00\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_NoLeadsWithMilli(t *testing.T) { func TestTOMLTest_Invalid_Datetime_NoLeadsWithMilli(t *testing.T) {
input := "with-milli = 1987-07-5T17:45:00.12Z\n" input := "# Day \"5\" instead of \"05\"; the leading zero is required.\nwith-milli = 1987-07-5T17:45:00.12Z\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_NoLeads(t *testing.T) { func TestTOMLTest_Invalid_Datetime_NoLeads(t *testing.T) {
input := "no-leads = 1987-7-05T17:45:00Z\n" input := "# Month \"7\" instead of \"07\"; the leading zero is required.\nno-leads = 1987-7-05T17:45:00Z\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_NoSecs(t *testing.T) { func TestTOMLTest_Invalid_Datetime_NoSecs(t *testing.T) {
input := "no-secs = 1987-07-05T17:45Z\n" input := "# No seconds in time.\nno-secs = 1987-07-05T17:45Z\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_NoT(t *testing.T) { func TestTOMLTest_Invalid_Datetime_NoT(t *testing.T) {
input := "no-t = 1987-07-0517:45:00Z\n" input := "# No \"t\" or \"T\" between the date and time.\nno-t = 1987-07-0517:45:00Z\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_SecondOver(t *testing.T) {
input := "# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n# ; rules\nd = 2006-01-01T00:00:61-00:00\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_TimeNoLeads2(t *testing.T) {
input := "# Leading 0 is always required.\nd = 01:32:0\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Datetime_TimeNoLeads(t *testing.T) {
input := "# Leading 0 is always required.\nd = 1:32:00\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Datetime_TrailingT(t *testing.T) { func TestTOMLTest_Invalid_Datetime_TrailingT(t *testing.T) {
input := "d = 2006-01-30T\n" input := "# Date cannot end with trailing T\nd = 2006-01-30T\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
@@ -395,6 +500,11 @@ func TestTOMLTest_Invalid_Float_UsBeforePoint(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_InlineTable_Add(t *testing.T) {
input := "a={}\n# Inline tables are immutable and can't be extended\n[a.b]\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_InlineTable_DoubleComma(t *testing.T) { func TestTOMLTest_Invalid_InlineTable_DoubleComma(t *testing.T) {
input := "t = {x=3,,y=4}\n" input := "t = {x=3,,y=4}\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -435,6 +545,11 @@ func TestTOMLTest_Invalid_InlineTable_NoComma(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_InlineTable_Overwrite(t *testing.T) {
input := "a.b=0\n# Since table \"a\" is already defined, it can't be replaced by an inline table.\na={}\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_InlineTable_TrailingComma(t *testing.T) { func TestTOMLTest_Invalid_InlineTable_TrailingComma(t *testing.T) {
input := "# A terminating comma (also called trailing comma) is not permitted after the\n# last key/value pair in an inline table\nabc = { abc = 123, }\n" input := "# A terminating comma (also called trailing comma) is not permitted after the\n# last key/value pair in an inline table\nabc = { abc = 123, }\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -470,6 +585,21 @@ func TestTOMLTest_Invalid_Integer_DoubleUs(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Integer_IncompleteBin(t *testing.T) {
input := "incomplete-bin = 0b\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Integer_IncompleteHex(t *testing.T) {
input := "incomplete-hex = 0x\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Integer_IncompleteOct(t *testing.T) {
input := "incomplete-oct = 0o\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Integer_InvalidBin(t *testing.T) { func TestTOMLTest_Invalid_Integer_InvalidBin(t *testing.T) {
input := "invalid-bin = 0b0012\n" input := "invalid-bin = 0b0012\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -515,6 +645,11 @@ func TestTOMLTest_Invalid_Integer_LeadingZero2(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Integer_LeadingZero3(t *testing.T) {
input := "leading-zero-3 = 0_0\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Integer_LeadingZeroSign1(t *testing.T) { func TestTOMLTest_Invalid_Integer_LeadingZeroSign1(t *testing.T) {
input := "leading-zero-sign-1 = -01\n" input := "leading-zero-sign-1 = -01\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -525,6 +660,11 @@ func TestTOMLTest_Invalid_Integer_LeadingZeroSign2(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Integer_LeadingZeroSign3(t *testing.T) {
input := "leading-zero-sign-3 = +0_1\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Integer_NegativeBin(t *testing.T) { func TestTOMLTest_Invalid_Integer_NegativeBin(t *testing.T) {
input := "negative-bin = -0b11010110\n" input := "negative-bin = -0b11010110\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -730,11 +870,16 @@ func TestTOMLTest_Invalid_String_BadConcat(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_String_BadEscape(t *testing.T) { func TestTOMLTest_Invalid_String_BadEscape1(t *testing.T) {
input := "invalid-escape = \"This string has a bad \\a escape character.\"\n" input := "invalid-escape = \"This string has a bad \\a escape character.\"\n"
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_String_BadEscape2(t *testing.T) {
input := "invalid-escape = \"This string has a bad \\ escape character.\"\n\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_String_BadMultiline(t *testing.T) { func TestTOMLTest_Invalid_String_BadMultiline(t *testing.T) {
input := "multi = \"first line\nsecond line\"\n" input := "multi = \"first line\nsecond line\"\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -805,6 +950,21 @@ func TestTOMLTest_Invalid_String_MissingQuotes(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_String_MultilineBadEscape1(t *testing.T) {
input := "k = \"\"\"t\\a\"\"\"\n\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_String_MultilineBadEscape2(t *testing.T) {
input := "# \\<Space> is not a valid escape.\nk = \"\"\"t\\ t\"\"\"\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_String_MultilineBadEscape3(t *testing.T) {
input := "# \\<Space> is not a valid escape.\nk = \"\"\"t\\ \"\"\"\n\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_String_MultilineEscapeSpace(t *testing.T) { func TestTOMLTest_Invalid_String_MultilineEscapeSpace(t *testing.T) {
input := "a = \"\"\"\n foo \\ \\n\n bar\"\"\"\n" input := "a = \"\"\"\n foo \\ \\n\n bar\"\"\"\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -825,11 +985,6 @@ func TestTOMLTest_Invalid_String_MultilineQuotes1(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_String_MultilineQuotes2(t *testing.T) {
input := "a = \"\"\"6 quotes: \"\"\"\"\"\"\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_String_NoClose(t *testing.T) { func TestTOMLTest_Invalid_String_NoClose(t *testing.T) {
input := "no-ending-quote = \"One time, at band camp\n" input := "no-ending-quote = \"One time, at band camp\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -845,6 +1000,16 @@ func TestTOMLTest_Invalid_String_WrongClose(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Table_AppendWithDottedKeys1(t *testing.T) {
input := "# First a.b.c defines a table: a.b.c = {z=9}\n#\n# Then we define a.b.c.t = \"str\" to add a str to the above table, making it:\n#\n# a.b.c = {z=9, t=\"...\"}\n#\n# While this makes sense, logically, it was decided this is not valid TOML as\n# it's too confusing/convoluted.\n# \n# See: https://github.com/toml-lang/toml/issues/846\n# https://github.com/toml-lang/toml/pull/859\n\n[a.b.c]\n z = 9\n\n[a]\n b.c.t = \"Using dotted keys to add to [a.b.c] after explicitly defining it above is not allowed\"\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_AppendWithDottedKeys2(t *testing.T) {
input := "# This is the same issue as in injection-1.toml, except that nests one level\n# deeper. See that file for a more complete description.\n\n[a.b.c.d]\n z = 9\n\n[a]\n b.c.d.k.t = \"Using dotted keys to add to [a.b.c.d] after explicitly defining it above is not allowed\"\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_ArrayEmpty(t *testing.T) { func TestTOMLTest_Invalid_Table_ArrayEmpty(t *testing.T) {
input := "[[]]\nname = \"Born to Run\"\n" input := "[[]]\nname = \"Born to Run\"\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -860,6 +1025,16 @@ func TestTOMLTest_Invalid_Table_ArrayMissingBracket(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Table_DuplicateKeyDottedTable(t *testing.T) {
input := "[fruit]\napple.color = \"red\"\n\n[fruit.apple] # INVALID\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_DuplicateKeyDottedTable2(t *testing.T) {
input := "[fruit]\napple.taste.sweet = true\n\n[fruit.apple.taste] # INVALID\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_DuplicateKeyTable(t *testing.T) { func TestTOMLTest_Invalid_Table_DuplicateKeyTable(t *testing.T) {
input := "[fruit]\ntype = \"apple\"\n\n[fruit.type]\napple = \"yes\"\n" input := "[fruit]\ntype = \"apple\"\n\n[fruit.type]\napple = \"yes\"\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -895,16 +1070,6 @@ func TestTOMLTest_Invalid_Table_EqualsSign(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Table_Injection1(t *testing.T) {
input := "[a.b.c]\n z = 9\n[a]\n b.c.t = \"Using dotted keys to add to [a.b.c] after explicitly defining it above is not allowed\"\n \n# see https://github.com/toml-lang/toml/issues/846\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_Injection2(t *testing.T) {
input := "[a.b.c.d]\n z = 9\n[a]\n b.c.d.k.t = \"Using dotted keys to add to [a.b.c.d] after explicitly defining it above is not allowed\"\n \n# see https://github.com/toml-lang/toml/issues/846\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Table_Llbrace(t *testing.T) { func TestTOMLTest_Invalid_Table_Llbrace(t *testing.T) {
input := "[ [table]]\n" input := "[ [table]]\n"
testgenInvalid(t, input) testgenInvalid(t, input)
@@ -1071,8 +1236,14 @@ func TestTOMLTest_Valid_Comment_AtEof2(t *testing.T) {
} }
func TestTOMLTest_Valid_Comment_Everywhere(t *testing.T) { func TestTOMLTest_Valid_Comment_Everywhere(t *testing.T) {
input := "# Top comment.\n # Top comment.\n# Top comment.\n\n# [no-extraneous-groups-please]\n\n[group] # Comment\nanswer = 42 # Comment\n# no-extraneous-keys-please = 999\n# Inbetween comment.\nmore = [ # Comment\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n 42, 42, # Comments within arrays are fun.\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n# ] Did I fool you?\n] # Hopefully not.\n\n# Make sure the space between the datetime and \"#\" isn't lexed.\nd = 1979-05-27T07:32:12-07:00 # c\n" input := "# Top comment.\n # Top comment.\n# Top comment.\n\n# [no-extraneous-groups-please]\n\n[group] # Comment\nanswer = 42 # Comment\n# no-extraneous-keys-please = 999\n# Inbetween comment.\nmore = [ # Comment\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n 42, 42, # Comments within arrays are fun.\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n# ] Did I fool you?\n] # Hopefully not.\n\n# Make sure the space between the datetime and \"#\" isn't lexed.\ndt = 1979-05-27T07:32:12-07:00 # c\nd = 1979-05-27 # Comment\n"
jsonRef := "{\n \"group\": {\n \"answer\": {\n \"type\": \"integer\",\n \"value\": \"42\"\n },\n \"d\": {\n \"type\": \"datetime\",\n \"value\": \"1979-05-27T07:32:12-07:00\"\n },\n \"more\": [\n {\n \"type\": \"integer\",\n \"value\": \"42\"\n },\n {\n \"type\": \"integer\",\n \"value\": \"42\"\n }\n ]\n }\n}\n" jsonRef := "{\n \"group\": {\n \"answer\": {\n \"type\": \"integer\",\n \"value\": \"42\"\n },\n \"dt\": {\n \"type\": \"datetime\",\n \"value\": \"1979-05-27T07:32:12-07:00\"\n },\n \"d\": {\n \"type\": \"date-local\",\n \"value\": \"1979-05-27\"\n },\n \"more\": [\n {\n \"type\": \"integer\",\n \"value\": \"42\"\n },\n {\n \"type\": \"integer\",\n \"value\": \"42\"\n }\n ]\n }\n}\n"
testgenValid(t, input, jsonRef)
}
func TestTOMLTest_Valid_Comment_Noeol(t *testing.T) {
input := "# single comment without any eol characters"
jsonRef := "{}\n"
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
@@ -1107,8 +1278,8 @@ func TestTOMLTest_Valid_Datetime_Local(t *testing.T) {
} }
func TestTOMLTest_Valid_Datetime_Milliseconds(t *testing.T) { func TestTOMLTest_Valid_Datetime_Milliseconds(t *testing.T) {
input := "utc1 = 1987-07-05T17:45:56.123456Z\nutc2 = 1987-07-05T17:45:56.6Z\nwita1 = 1987-07-05T17:45:56.123456+08:00\nwita2 = 1987-07-05T17:45:56.6+08:00\n" input := "utc1 = 1987-07-05T17:45:56.1234Z\nutc2 = 1987-07-05T17:45:56.6Z\nwita1 = 1987-07-05T17:45:56.1234+08:00\nwita2 = 1987-07-05T17:45:56.6+08:00\n"
jsonRef := "{\n \"utc1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.123456Z\"\n },\n \"utc2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.600000Z\"\n },\n \"wita1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.123456+08:00\"\n },\n \"wita2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.600000+08:00\"\n }\n}\n" jsonRef := "{\n \"utc1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.1234Z\"\n },\n \"utc2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.6000Z\"\n },\n \"wita1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.1234+08:00\"\n },\n \"wita2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.6000+08:00\"\n }\n}\n"
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
@@ -1370,6 +1541,12 @@ func TestTOMLTest_Valid_String_Empty(t *testing.T) {
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
func TestTOMLTest_Valid_String_EscapeEsc(t *testing.T) {
input := "esc = \"\\e There is no escape! \\e\"\n"
jsonRef := "{\n \"esc\": {\n \"type\": \"string\",\n \"value\": \"\\u001b There is no escape! \\u001b\"\n }\n}\n"
testgenValid(t, input, jsonRef)
}
func TestTOMLTest_Valid_String_EscapeTricky(t *testing.T) { func TestTOMLTest_Valid_String_EscapeTricky(t *testing.T) {
input := "end_esc = \"String does not end here\\\" but ends here\\\\\"\nlit_end_esc = 'String ends here\\'\n\nmultiline_unicode = \"\"\"\n\\u00a0\"\"\"\n\nmultiline_not_unicode = \"\"\"\n\\\\u0041\"\"\"\n\nmultiline_end_esc = \"\"\"When will it end? \\\"\"\"...\"\"\\\" should be here\\\"\"\"\"\n\nlit_multiline_not_unicode = '''\n\\u007f'''\n\nlit_multiline_end = '''There is no escape\\'''\n" input := "end_esc = \"String does not end here\\\" but ends here\\\\\"\nlit_end_esc = 'String ends here\\'\n\nmultiline_unicode = \"\"\"\n\\u00a0\"\"\"\n\nmultiline_not_unicode = \"\"\"\n\\\\u0041\"\"\"\n\nmultiline_end_esc = \"\"\"When will it end? \\\"\"\"...\"\"\\\" should be here\\\"\"\"\"\n\nlit_multiline_not_unicode = '''\n\\u007f'''\n\nlit_multiline_end = '''There is no escape\\'''\n"
jsonRef := "{\n \"end_esc\": {\n \"type\": \"string\",\n \"value\": \"String does not end here\\\" but ends here\\\\\"\n },\n \"lit_end_esc\": {\n \"type\": \"string\",\n \"value\": \"String ends here\\\\\"\n },\n \"lit_multiline_end\": {\n \"type\": \"string\",\n \"value\": \"There is no escape\\\\\"\n },\n \"lit_multiline_not_unicode\": {\n \"type\": \"string\",\n \"value\": \"\\\\u007f\"\n },\n \"multiline_end_esc\": {\n \"type\": \"string\",\n \"value\": \"When will it end? \\\"\\\"\\\"...\\\"\\\"\\\" should be here\\\"\"\n },\n \"multiline_not_unicode\": {\n \"type\": \"string\",\n \"value\": \"\\\\u0041\"\n },\n \"multiline_unicode\": {\n \"type\": \"string\",\n \"value\": \"\u00a0\"\n }\n}\n" jsonRef := "{\n \"end_esc\": {\n \"type\": \"string\",\n \"value\": \"String does not end here\\\" but ends here\\\\\"\n },\n \"lit_end_esc\": {\n \"type\": \"string\",\n \"value\": \"String ends here\\\\\"\n },\n \"lit_multiline_end\": {\n \"type\": \"string\",\n \"value\": \"There is no escape\\\\\"\n },\n \"lit_multiline_not_unicode\": {\n \"type\": \"string\",\n \"value\": \"\\\\u007f\"\n },\n \"multiline_end_esc\": {\n \"type\": \"string\",\n \"value\": \"When will it end? \\\"\\\"\\\"...\\\"\\\"\\\" should be here\\\"\"\n },\n \"multiline_not_unicode\": {\n \"type\": \"string\",\n \"value\": \"\\\\u0041\"\n },\n \"multiline_unicode\": {\n \"type\": \"string\",\n \"value\": \"\u00a0\"\n }\n}\n"
@@ -1388,14 +1565,20 @@ func TestTOMLTest_Valid_String_Escapes(t *testing.T) {
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
func TestTOMLTest_Valid_String_MultilineEscapedCrlf(t *testing.T) {
input := "# The following line should be an unescaped backslash followed by a Windows\r\n# newline sequence (\"\\r\\n\")\r\n0=\"\"\"\\\r\n\"\"\"\r\n"
jsonRef := "{\n \"0\": {\n \"type\": \"string\",\n \"value\": \"\"\n }\n}\n"
testgenValid(t, input, jsonRef)
}
func TestTOMLTest_Valid_String_MultilineQuotes(t *testing.T) { func TestTOMLTest_Valid_String_MultilineQuotes(t *testing.T) {
input := "# Make sure that quotes inside multiline strings are allowed, including right\n# after the opening '''/\"\"\" and before the closing '''/\"\"\"\n\nlit_one = ''''one quote''''\nlit_two = '''''two quotes'''''\nlit_one_space = ''' 'one quote' '''\nlit_two_space = ''' ''two quotes'' '''\n\none = \"\"\"\"one quote\"\"\"\"\ntwo = \"\"\"\"\"two quotes\"\"\"\"\"\none_space = \"\"\" \"one quote\" \"\"\"\ntwo_space = \"\"\" \"\"two quotes\"\" \"\"\"\n\nmismatch1 = \"\"\"aaa'''bbb\"\"\"\nmismatch2 = '''aaa\"\"\"bbb'''\n" input := "# Make sure that quotes inside multiline strings are allowed, including right\n# after the opening '''/\"\"\" and before the closing '''/\"\"\"\n\nlit_one = ''''one quote''''\nlit_two = '''''two quotes'''''\nlit_one_space = ''' 'one quote' '''\nlit_two_space = ''' ''two quotes'' '''\n\none = \"\"\"\"one quote\"\"\"\"\ntwo = \"\"\"\"\"two quotes\"\"\"\"\"\none_space = \"\"\" \"one quote\" \"\"\"\ntwo_space = \"\"\" \"\"two quotes\"\" \"\"\"\n\nmismatch1 = \"\"\"aaa'''bbb\"\"\"\nmismatch2 = '''aaa\"\"\"bbb'''\n\n# Three opening \"\"\", then one escaped \", then two \"\" (allowed), and then three\n# closing \"\"\"\nescaped = \"\"\"lol\\\"\"\"\"\"\"\n"
jsonRef := "{\n \"lit_one\": {\n \"type\": \"string\",\n \"value\": \"'one quote'\"\n },\n \"lit_one_space\": {\n \"type\": \"string\",\n \"value\": \" 'one quote' \"\n },\n \"lit_two\": {\n \"type\": \"string\",\n \"value\": \"''two quotes''\"\n },\n \"lit_two_space\": {\n \"type\": \"string\",\n \"value\": \" ''two quotes'' \"\n },\n \"mismatch1\": {\n \"type\": \"string\",\n \"value\": \"aaa'''bbb\"\n },\n \"mismatch2\": {\n \"type\": \"string\",\n \"value\": \"aaa\\\"\\\"\\\"bbb\"\n },\n \"one\": {\n \"type\": \"string\",\n \"value\": \"\\\"one quote\\\"\"\n },\n \"one_space\": {\n \"type\": \"string\",\n \"value\": \" \\\"one quote\\\" \"\n },\n \"two\": {\n \"type\": \"string\",\n \"value\": \"\\\"\\\"two quotes\\\"\\\"\"\n },\n \"two_space\": {\n \"type\": \"string\",\n \"value\": \" \\\"\\\"two quotes\\\"\\\" \"\n }\n}\n" jsonRef := "{\n \"escaped\": {\n \"type\": \"string\",\n \"value\": \"lol\\\"\\\"\\\"\"\n },\n \"lit_one\": {\n \"type\": \"string\",\n \"value\": \"'one quote'\"\n },\n \"lit_one_space\": {\n \"type\": \"string\",\n \"value\": \" 'one quote' \"\n },\n \"lit_two\": {\n \"type\": \"string\",\n \"value\": \"''two quotes''\"\n },\n \"lit_two_space\": {\n \"type\": \"string\",\n \"value\": \" ''two quotes'' \"\n },\n \"mismatch1\": {\n \"type\": \"string\",\n \"value\": \"aaa'''bbb\"\n },\n \"mismatch2\": {\n \"type\": \"string\",\n \"value\": \"aaa\\\"\\\"\\\"bbb\"\n },\n \"one\": {\n \"type\": \"string\",\n \"value\": \"\\\"one quote\\\"\"\n },\n \"one_space\": {\n \"type\": \"string\",\n \"value\": \" \\\"one quote\\\" \"\n },\n \"two\": {\n \"type\": \"string\",\n \"value\": \"\\\"\\\"two quotes\\\"\\\"\"\n },\n \"two_space\": {\n \"type\": \"string\",\n \"value\": \" \\\"\\\"two quotes\\\"\\\" \"\n }\n}\n"
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
func TestTOMLTest_Valid_String_Multiline(t *testing.T) { func TestTOMLTest_Valid_String_Multiline(t *testing.T) {
input := "# NOTE: this file includes some literal tab characters.\n\nmultiline_empty_one = \"\"\"\"\"\"\nmultiline_empty_two = \"\"\"\n\"\"\"\nmultiline_empty_three = \"\"\"\\\n \"\"\"\nmultiline_empty_four = \"\"\"\\\n \\\n \\ \n \"\"\"\n\nequivalent_one = \"The quick brown fox jumps over the lazy dog.\"\nequivalent_two = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"\n\nequivalent_three = \"\"\"\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n \"\"\"\n\nwhitespace-after-bs = \"\"\"\\\n The quick brown \\\n fox jumps over \\ \n the lazy dog.\\\t\n \"\"\"\n\nno-space = \"\"\"a\\\n b\"\"\"\n\nkeep-ws-before = \"\"\"a \t\\\n b\"\"\"\n\nescape-bs-1 = \"\"\"a \\\\\nb\"\"\"\n\nescape-bs-2 = \"\"\"a \\\\\\\nb\"\"\"\n\nescape-bs-3 = \"\"\"a \\\\\\\\\n b\"\"\"\n" input := "# NOTE: this file includes some literal tab characters.\n\nmultiline_empty_one = \"\"\"\"\"\"\n\n# A newline immediately following the opening delimiter will be trimmed.\nmultiline_empty_two = \"\"\"\n\"\"\"\n\n# \\ at the end of line trims newlines as well; note that last \\ is followed by\n# two spaces, which are ignored.\nmultiline_empty_three = \"\"\"\\\n \"\"\"\nmultiline_empty_four = \"\"\"\\\n \\\n \\ \n \"\"\"\n\nequivalent_one = \"The quick brown fox jumps over the lazy dog.\"\nequivalent_two = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"\n\nequivalent_three = \"\"\"\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n \"\"\"\n\nwhitespace-after-bs = \"\"\"\\\n The quick brown \\\n fox jumps over \\ \n the lazy dog.\\\t\n \"\"\"\n\nno-space = \"\"\"a\\\n b\"\"\"\n\n# Has tab character.\nkeep-ws-before = \"\"\"a \t\\\n b\"\"\"\n\nescape-bs-1 = \"\"\"a \\\\\nb\"\"\"\n\nescape-bs-2 = \"\"\"a \\\\\\\nb\"\"\"\n\nescape-bs-3 = \"\"\"a \\\\\\\\\n b\"\"\"\n"
jsonRef := "{\n \"equivalent_one\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"equivalent_three\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"equivalent_two\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"escape-bs-1\": {\n \"type\": \"string\",\n \"value\": \"a \\\\\\nb\"\n },\n \"escape-bs-2\": {\n \"type\": \"string\",\n \"value\": \"a \\\\b\"\n },\n \"escape-bs-3\": {\n \"type\": \"string\",\n \"value\": \"a \\\\\\\\\\n b\"\n },\n \"keep-ws-before\": {\n \"type\": \"string\",\n \"value\": \"a \\tb\"\n },\n \"multiline_empty_four\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_one\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_three\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_two\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"no-space\": {\n \"type\": \"string\",\n \"value\": \"ab\"\n },\n \"whitespace-after-bs\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n }\n}\n" jsonRef := "{\n \"equivalent_one\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"equivalent_three\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"equivalent_two\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n },\n \"escape-bs-1\": {\n \"type\": \"string\",\n \"value\": \"a \\\\\\nb\"\n },\n \"escape-bs-2\": {\n \"type\": \"string\",\n \"value\": \"a \\\\b\"\n },\n \"escape-bs-3\": {\n \"type\": \"string\",\n \"value\": \"a \\\\\\\\\\n b\"\n },\n \"keep-ws-before\": {\n \"type\": \"string\",\n \"value\": \"a \\tb\"\n },\n \"multiline_empty_four\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_one\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_three\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"multiline_empty_two\": {\n \"type\": \"string\",\n \"value\": \"\"\n },\n \"no-space\": {\n \"type\": \"string\",\n \"value\": \"ab\"\n },\n \"whitespace-after-bs\": {\n \"type\": \"string\",\n \"value\": \"The quick brown fox jumps over the lazy dog.\"\n }\n}\n"
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
@@ -1407,7 +1590,7 @@ func TestTOMLTest_Valid_String_Nl(t *testing.T) {
} }
func TestTOMLTest_Valid_String_RawMultiline(t *testing.T) { func TestTOMLTest_Valid_String_RawMultiline(t *testing.T) {
input := "oneline = '''This string has a ' quote character.'''\nfirstnl = '''\nThis string has a ' quote character.'''\nmultiline = '''\nThis string\nhas ' a quote character\nand more than\none newline\nin it.'''\n" input := "# Single ' should be allowed.\noneline = '''This string has a ' quote character.'''\n\n# A newline immediately following the opening delimiter will be trimmed.\nfirstnl = '''\nThis string has a ' quote character.'''\n\n# All other whitespace and newline characters remain intact.\nmultiline = '''\nThis string\nhas ' a quote character\nand more than\none newline\nin it.'''\n"
jsonRef := "{\n \"firstnl\": {\n \"type\": \"string\",\n \"value\": \"This string has a ' quote character.\"\n },\n \"multiline\": {\n \"type\": \"string\",\n \"value\": \"This string\\nhas ' a quote character\\nand more than\\none newline\\nin it.\"\n },\n \"oneline\": {\n \"type\": \"string\",\n \"value\": \"This string has a ' quote character.\"\n }\n}\n" jsonRef := "{\n \"firstnl\": {\n \"type\": \"string\",\n \"value\": \"This string has a ' quote character.\"\n },\n \"multiline\": {\n \"type\": \"string\",\n \"value\": \"This string\\nhas ' a quote character\\nand more than\\none newline\\nin it.\"\n },\n \"oneline\": {\n \"type\": \"string\",\n \"value\": \"This string has a ' quote character.\"\n }\n}\n"
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
+109 -29
View File
@@ -42,21 +42,22 @@ func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r} return &Decoder{r: r}
} }
// SetStrict toggles decoding in stict mode. // DisallowUnknownFields causes the Decoder to return an error when the
// destination is a struct and the input contains a key that does not match a
// non-ignored field.
// //
// When the decoder is in strict mode, it will record fields from the document // In that case, the Decoder returns a StrictMissingError that can be used to
// that could not be set on the target value. In that case, the decoder returns // retrieve the individual errors as well as generate a human readable
// a StrictMissingError that can be used to retrieve the individual errors as // description of the missing fields.
// well as generate a human readable description of the missing fields. func (d *Decoder) DisallowUnknownFields() *Decoder {
func (d *Decoder) SetStrict(strict bool) *Decoder { d.strict = true
d.strict = strict
return d return d
} }
// Decode the whole content of r into v. // Decode the whole content of r into v.
// //
// By default, values in the document that don't exist in the target Go value // By default, values in the document that don't exist in the target Go value
// are ignored. See Decoder.SetStrict() to change this behavior. // are ignored. See Decoder.DisallowUnknownFields() to change this behavior.
// //
// When a TOML local date, time, or date-time is decoded into a time.Time, its // When a TOML local date, time, or date-time is decoded into a time.Time, its
// value is represented in time.Local timezone. Otherwise the approriate Local* // value is represented in time.Local timezone. Otherwise the approriate Local*
@@ -229,15 +230,6 @@ func (d *decoder) fromParser(root reflect.Value) error {
return d.p.Error() return d.p.Error()
} }
/*
Rules for the unmarshal code:
- The stack is used to keep track of which values need to be set where.
- handle* functions <=> switch on a given ast.Kind.
- unmarshalX* functions need to unmarshal a node of kind X.
- An "object" is either a struct or a map.
*/
func (d *decoder) handleRootExpression(expr *ast.Node, v reflect.Value) error { func (d *decoder) handleRootExpression(expr *ast.Node, v reflect.Value) error {
var x reflect.Value var x reflect.Value
var err error var err error
@@ -321,10 +313,12 @@ func (d *decoder) handleArrayTableCollectionLast(key ast.Iterator, v reflect.Val
return v, nil return v, nil
case reflect.Slice: case reflect.Slice:
elemType := v.Type().Elem() elemType := v.Type().Elem()
var elem reflect.Value
if elemType.Kind() == reflect.Interface { if elemType.Kind() == reflect.Interface {
elemType = mapStringInterfaceType elem = makeMapStringInterface()
} else {
elem = reflect.New(elemType).Elem()
} }
elem := reflect.New(elemType).Elem()
elem2, err := d.handleArrayTable(key, elem) elem2, err := d.handleArrayTable(key, elem)
if err != nil { if err != nil {
return reflect.Value{}, err return reflect.Value{}, err
@@ -397,6 +391,84 @@ func (d *decoder) handleArrayTableCollection(key ast.Iterator, v reflect.Value)
return d.handleArrayTable(key, v) return d.handleArrayTable(key, v)
} }
func (d *decoder) handleKeyValuePartMapStringInterface(key ast.Iterator, value *ast.Node, m map[string]interface{}) (reflect.Value, error) {
k := string(key.Node().Data)
newMap := false
if m == nil {
newMap = true
m = make(map[string]interface{}, 8)
}
set := false
v, ok := m[k]
if !ok || key.IsLast() {
set = true
v = nil
}
mv := reflect.ValueOf(&v).Elem()
x, err := d.handleKeyValueInner(key, value, mv)
if err != nil {
return reflect.Value{}, err
}
if x.IsValid() {
mv = x
set = true
}
if set {
m[k] = mv.Interface()
}
if newMap {
return reflect.ValueOf(m), nil
}
return reflect.Value{}, nil
}
func (d *decoder) handleKeyPartMapStringInterface(key ast.Iterator, m map[string]interface{}, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) {
newMap := false
k := string(key.Node().Data)
if m == nil {
newMap = true
m = make(map[string]interface{}, 8)
}
v, ok := m[k]
set := false
if !ok || v == nil {
set = true
v = makeFn().Interface()
}
mv := reflect.ValueOf(v)
x, err := nextFn(key, mv)
if err != nil {
return reflect.Value{}, err
}
if x.IsValid() {
mv = x
set = true
}
if set {
m[k] = mv.Interface()
}
if newMap {
return reflect.ValueOf(m), nil
}
return reflect.Value{}, nil
}
func (d *decoder) handleKeyPart(key ast.Iterator, v reflect.Value, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) { func (d *decoder) handleKeyPart(key ast.Iterator, v reflect.Value, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) {
var rv reflect.Value var rv reflect.Value
@@ -411,9 +483,15 @@ func (d *decoder) handleKeyPart(key ast.Iterator, v reflect.Value, nextFn handle
elem = v.Elem() elem = v.Elem()
return d.handleKeyPart(key, elem, nextFn, makeFn) return d.handleKeyPart(key, elem, nextFn, makeFn)
case reflect.Map: case reflect.Map:
vt := v.Type()
// Create the key for the map element. For now assume it's a string. if vt == mapStringInterfaceType {
mk := reflect.ValueOf(string(key.Node().Data)) m := v.Interface().(map[string]interface{})
return d.handleKeyPartMapStringInterface(key, m, nextFn, makeFn)
}
// Create the key for the map element. Convert to key type.
mk := reflect.ValueOf(string(key.Node().Data)).Convert(vt.Key())
// If the map does not exist, create it. // If the map does not exist, create it.
if v.IsNil() { if v.IsNil() {
@@ -430,7 +508,6 @@ func (d *decoder) handleKeyPart(key ast.Iterator, v reflect.Value, nextFn handle
// map[string]interface{} or a []interface{} depending on whether // map[string]interface{} or a []interface{} depending on whether
// this is the last part of the array table key. // this is the last part of the array table key.
vt := v.Type()
t := vt.Elem() t := vt.Elem()
if t.Kind() == reflect.Interface { if t.Kind() == reflect.Interface {
mv = makeFn() mv = makeFn()
@@ -610,7 +687,7 @@ func (d *decoder) tryTextUnmarshaler(node *ast.Node, v reflect.Value) (bool, err
if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) { if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) {
err := v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data) err := v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data)
if err != nil { if err != nil {
return false, newDecodeError(d.p.Raw(node.Raw), "error calling UnmarshalText: %w", err) return false, newDecodeError(d.p.Raw(node.Raw), "%w", err)
} }
return true, nil return true, nil
@@ -996,6 +1073,11 @@ func (d *decoder) handleKeyValuePart(key ast.Iterator, value *ast.Node, v reflec
case reflect.Map: case reflect.Map:
vt := v.Type() vt := v.Type()
if vt == mapStringInterfaceType {
m := v.Interface().(map[string]interface{})
return d.handleKeyValuePartMapStringInterface(key, value, m)
}
mk := reflect.ValueOf(string(key.Node().Data)) mk := reflect.ValueOf(string(key.Node().Data))
mkt := stringType mkt := stringType
@@ -1019,12 +1101,10 @@ func (d *decoder) handleKeyValuePart(key ast.Iterator, value *ast.Node, v reflec
if !mv.IsValid() { if !mv.IsValid() {
set = true set = true
mv = reflect.New(v.Type().Elem()).Elem() mv = reflect.New(v.Type().Elem()).Elem()
} else { } else if key.IsLast() {
if key.IsLast() { var x interface{}
var x interface{} mv = reflect.ValueOf(&x).Elem()
mv = reflect.ValueOf(&x).Elem() set = true
set = true
}
} }
nv, err := d.handleKeyValueInner(key, value, mv) nv, err := d.handleKeyValueInner(key, value, mv)
+39 -6
View File
@@ -16,7 +16,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func ExampleDecoder_SetStrict() { func ExampleDecoder_DisallowUnknownFields() {
type S struct { type S struct {
Key1 string Key1 string
Key3 string Key3 string
@@ -28,7 +28,7 @@ key3 = "value3"
` `
r := strings.NewReader(doc) r := strings.NewReader(doc)
d := toml.NewDecoder(r) d := toml.NewDecoder(r)
d.SetStrict(true) d.DisallowUnknownFields()
s := S{} s := S{}
err := d.Decode(&s) err := d.Decode(&s)
@@ -279,6 +279,11 @@ func TestUnmarshal_Floats(t *testing.T) {
input: `1.0_e2`, input: `1.0_e2`,
err: true, err: true,
}, },
{
desc: "leading zero in positive float",
input: `+0_0.0`,
err: true,
},
} }
type doc struct { type doc struct {
@@ -540,6 +545,35 @@ func TestUnmarshal(t *testing.T) {
} }
}, },
}, },
{
desc: "issue 739 - table redefinition",
input: `
[foo.bar.baz]
wibble = 'wobble'
[foo]
[foo.bar]
huey = 'dewey'
`,
gen: func() test {
m := map[string]interface{}{}
return test{
target: &m,
expected: &map[string]interface{}{
`foo`: map[string]interface{}{
"bar": map[string]interface{}{
"huey": "dewey",
"baz": map[string]interface{}{
"wibble": "wobble",
},
},
},
},
}
},
},
{ {
desc: "multiline basic string", desc: "multiline basic string",
input: `A = """\ input: `A = """\
@@ -937,7 +971,7 @@ B = "data"`,
"Name": "Hammer", "Name": "Hammer",
"Sku": int64(738594937), "Sku": int64(738594937),
}, },
map[string]interface{}(nil), map[string]interface{}{},
map[string]interface{}{ map[string]interface{}{
"Name": "Nail", "Name": "Nail",
"Sku": int64(284758393), "Sku": int64(284758393),
@@ -1471,7 +1505,7 @@ B = "data"`,
target: &map[string]interface{}{}, target: &map[string]interface{}{},
expected: &map[string]interface{}{ expected: &map[string]interface{}{
"products": []interface{}{ "products": []interface{}{
map[string]interface{}(nil), map[string]interface{}{},
}, },
}, },
} }
@@ -1901,7 +1935,7 @@ bar = 42
t.Run("strict", func(t *testing.T) { t.Run("strict", func(t *testing.T) {
r := strings.NewReader(e.input) r := strings.NewReader(e.input)
d := toml.NewDecoder(r) d := toml.NewDecoder(r)
d.SetStrict(true) d.DisallowUnknownFields()
x := e.target x := e.target
if x == nil { if x == nil {
x = &struct{}{} x = &struct{}{}
@@ -1919,7 +1953,6 @@ bar = 42
t.Run("default", func(t *testing.T) { t.Run("default", func(t *testing.T) {
r := strings.NewReader(e.input) r := strings.NewReader(e.input)
d := toml.NewDecoder(r) d := toml.NewDecoder(r)
d.SetStrict(false)
x := e.target x := e.target
if x == nil { if x == nil {
x = &struct{}{} x = &struct{}{}