Compare commits

...

160 Commits

Author SHA1 Message Date
大可 a3d5a0bb53 fix: sync pool race condition (#947) 2024-04-29 06:02:54 -04:00
Daniel Weiße d00d2cca6e Fix indentation of custom type arrays (#944)
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
2024-04-12 10:42:12 -04:00
dependabot[bot] 86608d7fca build(deps): bump github/codeql-action from 2 to 3 (#919)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 13:24:46 -04:00
dependabot[bot] 4a1877957a build(deps): bump actions/setup-go from 4 to 5 (#916)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 13:24:37 -04:00
dependabot[bot] 3021d6d033 build(deps): bump actions/upload-artifact from 3 to 4 (#920)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 13:24:24 -04:00
Thomas Pelletier 32788f26f8 Update release instructions (#941) 2024-03-19 12:47:39 -04:00
rszyma 8ed6d131eb Decode: unstable/Unmarshal interface (#940)
Co-authored-by: Pavlos Karakalidis <pkarakal@pkarakal.com>
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2024-03-19 12:33:12 -04:00
dependabot[bot] 7dad87762a build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#936)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 09:06:45 -04:00
Thomas Pelletier 2b69615b5d Go 1.22 support (#935) 2024-02-27 15:30:13 -05:00
Thomas Pelletier 06fb30bf2e Decode: fix reuse of slice for array tables (#934)
When decoding into a non-empty slice, it needs to be emptied so that only the
tables contained in the document are present in the resulting value.

Arrays are not impacted because their unmarshal offset is tracked separately.

Fixes #931
2024-02-27 15:28:49 -05:00
Thomas Pelletier 2e087bdf5f Run tests on macos/M1 (#929)
https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/
2024-01-30 19:15:50 -05:00
Thomas Pelletier caeb9f9631 Fix marshaler typos (#927) 2024-01-30 19:01:55 -05:00
Rdbo e7223fb40e fix: odd indentation in README (#928) 2024-01-30 19:01:43 -05:00
Jakub Wilk 05bedf36d8 Fix README typo (#925) 2024-01-25 15:21:33 -08:00
Daniel Graña f5486d590f Support encoding json.Number type (#923)
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2024-01-25 15:21:02 -08:00
Daniel Graña 2ca21fb7b4 Support encoding of pointers to embedded structs (#924) 2024-01-23 13:06:33 -05:00
Thomas Pelletier 34765b4a9e Fix unmarshaling of nested non-exported struct (#917)
Fixes #915
2023-12-11 14:17:49 -05:00
Moritz Poldrack 358c8d2c23 Use toml-test to generate tests (#911)
Fixes: #909
2023-10-26 12:05:02 -06:00
Martin Tournoij fd8d0bf4d9 Add cmd/gotoml-test-encoder (#907) 2023-10-23 14:40:44 -06:00
Thomas Pelletier a76e18e8c5 Fix benchmark script (#905) 2023-10-02 13:49:01 -04:00
dependabot[bot] dff0c128d0 build(deps): bump docker/login-action from 2 to 3 (#901)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-17 18:11:24 -04:00
Thomas Pelletier 3573ce3770 Update SECURITY.md
Remove placeholder.
2023-09-04 09:43:32 -04:00
dependabot[bot] ae933f2e2a build(deps): bump actions/checkout from 3 to 4 (#896)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 09:42:37 -04:00
Thomas Pelletier 3175efb395 encode: fix commented table with comment (#894)
And updated README.
2023-08-29 08:46:34 -04:00
Thomas Pelletier 9dd7f1af78 encoder: add back the commented option (#893) 2023-08-28 14:29:26 -04:00
Mikhail f. Shiryaev 4a5c27c299 decode: fix wrong indention for tables' comments (#892) 2023-08-28 13:23:11 -04:00
Thomas Pelletier 76cc96f6d8 Update LICENSE 2023-08-28 11:22:32 -04:00
Thomas Pelletier 4835627845 Decode: improve errors on integers and strings (#891) 2023-08-28 11:17:48 -04:00
Thomas Pelletier cef80b96a4 parser: add raw to integers (#890) 2023-08-28 11:02:16 -04:00
Thomas Pelletier 4040373cfd Encode: fix ignored indent of array tables (#889)
Fixes #888
2023-08-28 09:52:11 -04:00
Haiyang Wang bb026cae89 Decode: fix panic when parsing '0' as a float (#887)
Fixes #886
2023-08-22 18:07:39 +02:00
michalbiesek f7d9b9ba53 Build riscv64 binaries for linux (#884)
Signed-off-by: Michal Biesek <michalbiesek@gmail.com>
2023-08-20 19:48:24 +02:00
michalbiesek fac33d6fa8 Add support for Go 1.21 (#885)
* Add support for `Go 1.21`

Signed-off-by: Michal Biesek <michalbiesek@gmail.com>

* add go1.21 guard to fuzz_test.go

* ci: only build last two go versions

* fix workflow yaml syntax error

---------

Signed-off-by: Michal Biesek <michalbiesek@gmail.com>
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2023-08-20 19:47:04 +02:00
David Barroso e183db7e69 Decode: assigned empty struct to empty defined sections (#879)
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2023-07-12 10:53:17 -04:00
dependabot[bot] 60e4af8cca build(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4 (#877)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.3...v1.8.4)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-30 09:12:07 -04:00
MrJetBOX 8bb1e08dc7 Encode: fix support for arrays (#876) 2023-05-29 09:41:33 -04:00
Thomas Pelletier 7b980e792b Use PtrTo to not require Go 1.18 (#874) 2023-05-23 18:22:22 -04:00
dependabot[bot] 44c1513ccd build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.3 (#871)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 10:46:40 -04:00
Thomas Pelletier fcf9d37d0c Comments support in unstable/Parser (#860) 2023-05-19 10:44:02 -04:00
Thomas Pelletier 986afffb7c Decode: fix decode into unsettable structs (#868)
Fixes #866
2023-05-16 09:29:50 -04:00
Thomas Pelletier 8c2c9cc986 Add example on how to use TextUnmarshaler (#867)
Ref #865
2023-05-16 08:17:21 -04:00
manunio 55ca4e35e4 fuzz: improve target perf (#864)
This attempts to improve issue types like timeout, slow_unit
and speed, as seen in the following oss-fuzz performance report:
https://oss-fuzz.com/performance-report/libFuzzer_go-toml_fuzz_toml/libfuzzer_asan_go-toml/2023-05-11
2023-05-12 16:21:17 +02:00
Gordon d34104d493 Support text Un/Marshaller for map keys (#863) 2023-05-09 17:56:57 +02:00
manunio 2aa08368fa fuzz: move fuzz_target from oss-fuzz (#861) 2023-04-28 21:38:13 -04:00
dependabot[bot] 654811fbc3 build(deps): bump actions/setup-go from 3 to 4 (#859)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 13:35:40 -04:00
dependabot[bot] 5c05d4d863 build(deps): bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#852)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 13:57:54 -04:00
Thomas Pelletier 643c251c4b Fix panic when unmarshaling into a map twice (#854)
Fixes #851
2023-02-28 17:34:24 +01:00
Thomas Pelletier 8a416daa69 Fix error report of type mismatch on inline tables (#853)
Parser did not track the location of the faulty inline table in the
document, and unmarshaler tried to the use the non-raw data field of the
AST node, both resulting into a panic when generating the parser error.

Fixes #850
2023-02-28 17:06:49 +01:00
Andreas Deininger fcd9179b7d Fixes typos (#849) 2023-02-13 03:57:48 -08:00
Marty 9f5726004e Allow integers to be unmarshaled into floats (#841)
Co-authored-by: Marty <martin@windscribe.com>
2023-02-09 12:02:25 -05:00
Thomas Pelletier c4a2eef8a4 Fix go 1.20 version in github actions (#848)
YAML interprets 1.20 as 1.2 without explicitely being a string.
2023-02-09 12:00:14 -05:00
Thomas Pelletier b58c20aa49 Upgrade go 1.20 (#847)
Fixes #842
2023-02-08 18:59:58 -05:00
Cuong Manh Le 090cccf4ba Fix inline table first key value whitespace (#837)
Co-authored-by: Cuong Manh Le <cuong@windscribe.com>
2023-02-01 12:00:09 +01:00
DavidKorczynski 58a592bbf8 ci: add CIFuzz integration (#831)
Signed-off-by: David Korczynski <david@adalogics.com>
2022-11-21 18:51:48 -05:00
dependabot[bot] 94bd3ddcd6 build(deps): bump actions/setup-go from 2 to 3 (#820)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-09 16:14:19 -05:00
Thomas Pelletier e195b58fd0 Expose parser API as unstable (#827) 2022-11-09 16:12:39 -05:00
dependabot[bot] c83d001c6d build(deps): bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#825)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-30 13:53:41 -04:00
Johanan Idicula b9e3b9c370 refactor: Use typeMismatchError rather than raw string error (#826)
Uses the existing method to DRY up the error message generation, and decorates
with position index where needed. No behaviour is changed, but it allows for
further changes to make error messaging more specific.

Related to: pelletier/go-toml#806
2022-10-30 13:44:16 -04:00
Olivier Mengué d26887310c Reduce init time allocation when declaring types used for reflect (#821)
In declaration of types used for reflect, use
reflect.TypeOf((*T)).Elem() instead of reflect.TypeOf(T{}) to avoid
init-time allocations.

See related stdlib issue: https://github.com/golang/go/issues/55973
2022-10-07 14:28:37 +02:00
Thomas Pelletier 942841787a Fix reflect.Pointer backward compatibility (#813)
Though we don't officially support older versions of Go, this is an easy fix to
unblock people.

Fixes #812
2022-08-26 09:15:03 -04:00
Thomas Pelletier 28f1efc7d3 Decode: don't break on non-struct embed field (#810) 2022-08-22 18:39:11 -04:00
Piotr Buliński 7d69e4a728 Add missing '+build' comment to fuzz_test.go (#809) 2022-08-22 14:05:37 -04:00
Thomas Pelletier e46d245c09 Decode: don't crash on embedded nil pointers (#808)
Also has the perks of reducing the overhead of FindByIndex:

```
name                                old time/op    new time/op    delta
UnmarshalDataset/config-32            17.0ms ± 1%    17.0ms ± 1%    ~     (p=1.000 n=5+5)
UnmarshalDataset/canada-32            71.6ms ± 1%    71.4ms ± 1%    ~     (p=1.000 n=5+5)
UnmarshalDataset/citm_catalog-32      24.2ms ± 3%    23.5ms ± 2%  -3.03%  (p=0.032 n=5+5)
UnmarshalDataset/twitter-32           9.37ms ± 1%    9.09ms ± 2%  -2.97%  (p=0.032 n=5+5)
UnmarshalDataset/code-32              75.4ms ± 2%    74.9ms ± 0%    ~     (p=0.222 n=5+5)
UnmarshalDataset/example-32            147µs ±10%     136µs ± 1%  -7.14%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-32     512ns ± 2%     500ns ± 0%  -2.35%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/map-32        721ns ± 2%     702ns ± 1%  -2.68%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-32     40.1µs ± 0%    39.6µs ± 0%  -1.30%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/map-32        62.3µs ± 1%    60.6µs ± 0%  -2.83%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-32          10.8µs ± 1%    10.5µs ± 1%  -2.86%  (p=0.008 n=5+5)

name                                old speed      new speed      delta
UnmarshalDataset/config-32          61.8MB/s ± 1%  61.8MB/s ± 1%    ~     (p=1.000 n=5+5)
UnmarshalDataset/canada-32          30.8MB/s ± 1%  30.8MB/s ± 1%    ~     (p=1.000 n=5+5)
UnmarshalDataset/citm_catalog-32    23.0MB/s ± 3%  23.8MB/s ± 2%  +3.09%  (p=0.032 n=5+5)
UnmarshalDataset/twitter-32         47.2MB/s ± 1%  48.6MB/s ± 2%  +3.09%  (p=0.032 n=5+5)
UnmarshalDataset/code-32            35.6MB/s ± 2%  35.9MB/s ± 0%    ~     (p=0.222 n=5+5)
UnmarshalDataset/example-32         55.3MB/s ±10%  59.4MB/s ± 1%  +7.36%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/struct-32  21.5MB/s ± 2%  22.0MB/s ± 0%  +2.41%  (p=0.008 n=5+5)
Unmarshal/SimpleDocument/map-32     15.2MB/s ± 2%  15.7MB/s ± 1%  +2.74%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/struct-32    131MB/s ± 0%   132MB/s ± 0%  +1.31%  (p=0.008 n=5+5)
Unmarshal/ReferenceFile/map-32      84.1MB/s ± 1%  86.6MB/s ± 0%  +2.91%  (p=0.008 n=5+5)
Unmarshal/HugoFrontMatter-32        50.6MB/s ± 1%  52.1MB/s ± 1%  +2.93%  (p=0.008 n=5+5)

name                                old alloc/op   new alloc/op   delta
UnmarshalDataset/config-32            5.86MB ± 0%    5.86MB ± 0%    ~     (p=0.579 n=5+5)
UnmarshalDataset/canada-32            83.0MB ± 0%    83.0MB ± 0%    ~     (p=0.651 n=5+5)
UnmarshalDataset/citm_catalog-32      34.7MB ± 0%    34.7MB ± 0%    ~     (p=0.548 n=5+5)
UnmarshalDataset/twitter-32           12.7MB ± 0%    12.7MB ± 0%    ~     (p=1.000 n=5+5)
UnmarshalDataset/code-32              22.2MB ± 0%    22.2MB ± 0%    ~     (p=0.841 n=5+5)
UnmarshalDataset/example-32            186kB ± 0%     186kB ± 0%    ~     (p=0.111 n=5+5)
Unmarshal/SimpleDocument/struct-32      805B ± 0%      805B ± 0%    ~     (all equal)
Unmarshal/SimpleDocument/map-32       1.13kB ± 0%    1.13kB ± 0%    ~     (all equal)
Unmarshal/ReferenceFile/struct-32     20.9kB ± 0%    20.9kB ± 0%    ~     (p=0.643 n=5+5)
Unmarshal/ReferenceFile/map-32        38.3kB ± 0%    38.3kB ± 0%    ~     (p=0.397 n=5+5)
Unmarshal/HugoFrontMatter-32          7.44kB ± 0%    7.44kB ± 0%    ~     (all equal)

name                                old allocs/op  new allocs/op  delta
UnmarshalDataset/config-32              227k ± 0%      227k ± 0%    ~     (p=1.000 n=5+5)
UnmarshalDataset/canada-32              782k ± 0%      782k ± 0%    ~     (all equal)
UnmarshalDataset/citm_catalog-32        192k ± 0%      192k ± 0%    ~     (p=0.968 n=4+5)
UnmarshalDataset/twitter-32            56.9k ± 0%     56.9k ± 0%    ~     (p=0.429 n=4+5)
UnmarshalDataset/code-32               1.05M ± 0%     1.05M ± 0%    ~     (p=0.556 n=4+5)
UnmarshalDataset/example-32            1.36k ± 0%     1.36k ± 0%    ~     (all equal)
Unmarshal/SimpleDocument/struct-32      9.00 ± 0%      9.00 ± 0%    ~     (all equal)
Unmarshal/SimpleDocument/map-32         13.0 ± 0%      13.0 ± 0%    ~     (all equal)
Unmarshal/ReferenceFile/struct-32        183 ± 0%       183 ± 0%    ~     (all equal)
Unmarshal/ReferenceFile/map-32           642 ± 0%       642 ± 0%    ~     (all equal)
Unmarshal/HugoFrontMatter-32             141 ± 0%       141 ± 0%    ~     (all equal)
```

Fixes #807
2022-08-20 21:24:03 -04:00
Thomas Pelletier 7baa23f493 Decode: error on array table mismatched type (#804)
Prevent the decoder from continuing if it encounters a type it cannot decode an
array table into.

Fixes #799
2022-08-15 16:38:07 -04:00
Thomas Pelletier 2d8433b69e Encode: don't inherit omitempty (#803)
Fixes #786.
2022-08-15 11:29:46 -04:00
Thomas Pelletier 67bc5422f3 Go 1.19 (#802) 2022-08-15 10:56:33 -04:00
Thomas Pelletier fb6d1d6c2b Marshal: define and fix newlines behavior when using omitempty (#798)
Ref #786
2022-07-24 15:40:20 -04:00
dependabot[bot] d017a6dc89 build(deps): bump github.com/stretchr/testify from 1.7.5 to 1.8.0 (#795) 2022-06-29 09:51:28 -04:00
dependabot[bot] d6d3196163 build(deps): bump github.com/stretchr/testify from 1.7.4 to 1.7.5 (#794) 2022-06-24 12:49:56 -04:00
dependabot[bot] 41718a6db3 build(deps): bump github.com/stretchr/testify from 1.7.2 to 1.7.4 (#793)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.2...v1.7.4)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-21 08:32:13 -04:00
Thomas Pelletier 216628222f Build arm + arm64 binaries for linux and windows (#790)
* Build arm + arm64 binaries for linux and windows

* Type MaxInt64 to avoid overflow on 32 bits arch

On a 32 bits arch, math.MaxIn64 is interpreted as an int, and therefore
overflows. This causes compilation to build on those platforms.
2022-06-08 18:05:42 -04:00
dependabot[bot] 322e0b15d2 build(deps): bump github.com/stretchr/testify from 1.7.1 to 1.7.2 (#788)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.1...v1.7.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-08 17:44:59 -04:00
Thomas Pelletier 85bfc0ed51 Encode: add bound check for uint64 > math.Int64 (#785)
As brought up on #782, there is an asymetry between numbers go-toml can encode
and decode. Specifically, unsigned numbers larger than `math.Int64` could be
encoded but not decoded.

We considered two options: allow decoding of those numbers, or prevent those
numbers to be decoded.

Ultimately we decided to narrow the range of numbers to be encoded. The TOML
specification only requires parsers to support at least the 64 bits integer
range. Allowing larger numbers would create non-standard TOML documents, which
may not be readable (at best) by other implementations. It is a safer default to
generate documents valid by default. People who wish to deal with larger numbers
can provide a custom type implementing `encoding.TextMarshaler`.

Refs #781
2022-05-31 22:10:41 -04:00
dependabot[bot] 295a720dfb build(deps): bump goreleaser/goreleaser-action from 2 to 3 (#783)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 2 to 3.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-23 15:05:00 +02:00
Thomas Pelletier 0a422e3dbd Decoder: check max uint on 32 bit platforms (#778)
Fixes #777
2022-05-10 15:43:26 +02:00
Thomas Pelletier 627dade0c7 Encode: support comment on array tables (#776)
Fixes #774
2022-05-10 15:17:36 +02:00
Thomas Pelletier b2e0231cc9 Encode: fix multiline comment (#775)
Fixes #768
2022-05-10 14:53:26 +02:00
dependabot[bot] ba95863cd3 build(deps): bump docker/login-action from 1 to 2 (#771)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-09 18:59:02 +02:00
Erik Hartwig db679df765 Typo in README.md fix (#770)
Change "document was not prevent in the target structure" to "document was not present in the target structure".
2022-05-09 18:46:46 +02:00
Thomas Pelletier c5ca2c682b Fix embedded struct with explicit field name (#773)
Fixes #772
2022-05-09 18:45:02 +02:00
Thomas Pelletier ed80712cb4 Copy version policy from v1 2022-04-28 11:55:39 -04:00
dependabot[bot] b24772942d build(deps): bump github/codeql-action from 1 to 2 (#764)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-28 08:24:14 -04:00
dependabot[bot] 9501a05ed7 build(deps): bump actions/checkout from 2 to 3 (#765)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-28 08:23:56 -04:00
Thomas Pelletier 171a592663 Prepare repository for v2.0.0 (#762) 2022-04-27 21:29:16 -04:00
Thomas Pelletier 5aaf5ef13b Fix internal entry size test (#761)
The test meant to assert that the size of entry does not grow beyond
what it is today. 48 is the current size in bytes on x64. On 32-bit
platform that value should be less. As written the test was doing the
opposite comparison.

Fixes #760
2022-04-24 18:50:54 -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
Thomas Pelletier 1b1dd3d6d5 Exclude testing PRs from release notes 2021-12-29 09:53:43 -05:00
Cameron Moore 128b7a8bfb Decode: check buffer length before parsing simple key (#717)
Fixes #714
2021-12-29 08:58:42 -05:00
Cameron Moore 892df5c28e Decode: fix index out of range bug (#716)
Fixes #715
2021-12-29 08:49:33 -05:00
Thomas Pelletier d58eb50ebf Doc: clarify errors returned by Decode (#713)
Fixes #625
2021-12-26 20:04:09 +01:00
Thomas Pelletier 535fc65c5f Fix link in README 2021-12-26 19:49:35 +01:00
Thomas Pelletier f158d7d278 Readme: document more differences with v1 (#712)
* Readme: document more changes with v1

Closes #552
2021-12-26 19:47:03 +01:00
Thomas Pelletier 5fd6e9cce0 Encode: add comment struct tag (#711)
Similar to v1, add a `comment` struct that that makes the encoder emit a comment
before the annotated element, if permitted. Unlike v1, comments are compact by
default (and cannot be changed).

Fixes #595.
2021-12-26 18:29:46 +01:00
Thomas Pelletier 8ce5c3d78f Decoder: time allows extra precision (#710)
As discussed[1], this change allows times to provide precision beyond the
nanosecond (nine digits fractional part). Extra precision is truncated according
to the TOML specificiation.

[1]: https://github.com/pelletier/go-toml/discussions/707
2021-12-26 17:05:10 +01:00
Thomas Pelletier 177b4a5e53 Decode: allow \r\n as line whitespace before \ (#709)
Fixes #708
2021-12-26 16:38:15 +01:00
Cameron Moore 5cbdea6192 decode: fix maximum time offset values (#706)
According to RFC3339 section 5.6, the maximum time offset values for
hours and minutes is 23 and 59, respectively.
2021-12-22 10:29:52 +01:00
Thomas Pelletier 696dd25c17 Decoder: disallow modification of existing table (#704)
Fixes #703
2021-12-15 11:05:27 -05:00
Thomas Pelletier facb2b13e8 Decoder: prevent modification of inline tables (#702)
Fixes #701
2021-12-12 09:43:42 -05:00
Cameron Moore 8bbb519477 Decode: ensure signed exponents don't start with an underscore (#699) 2021-12-05 20:02:19 -05:00
Cameron Moore b37e11d74d Decode: allow maximum seconds value of 60 (#700)
RFC3339 allows seconds to be 60 when adding leap seconds
2021-12-05 20:00:42 -05:00
Cameron Moore 6cd86876b8 Decode: ensure signed numbers don't start with an underscore (#698) 2021-12-04 16:56:48 -05:00
Cameron Moore f53bc740c1 Decode: restrict timezone offset values (#696)
Don't allow hours greater than 24 and minutes greater than 60 per RFC
3339.
2021-12-02 18:59:32 -05:00
Thomas Pelletier 9bf9be681e Decoder: check for invalid chars in timezone (#695)
Fixes #694
2021-12-02 09:00:20 -05:00
Thomas Pelletier c862c344b3 Decoder: allow commas in tags (#693) 2021-11-30 21:59:22 -05:00
Thomas Pelletier 0d20a84523 Encoder: omitempty flag (#692)
Fixes #597
2021-11-30 21:32:28 -05:00
Thomas Pelletier 3990899d7e Decoder: check tz has : between hours and minutes (#691)
Fixes #690
2021-11-30 20:22:11 -05:00
Cameron Moore 4c7a337083 Decoder: fix typo in test description (#689) 2021-11-30 15:28:01 -05:00
Thomas Pelletier bbaae540ce Decoder: check timezones start with +,-,z,Z (#688)
Also simplifies local time seconds scanning.

Fixes #686
2021-11-30 13:01:15 -05:00
Thomas Pelletier ede6445608 Decoder: flag bad \r in literal multiline strings (#687)
Fixes #685
2021-11-30 10:44:48 -05:00
Thomas Pelletier b226db6a29 Decoder: show struct field in type mismatch errors (#684)
The goal is to provide some context as to why the type were mismatched. This
change only works for that case, on structs. This is the same a encoding/json. A
more general solution would be great, but this would require a broader change in
the decoder, which I don't think is necessary at the moment.

Fixes #628
2021-11-24 20:43:56 -05:00
Thomas Pelletier d8997efb5a Mention "-" to prevent encoding field in doc (#683) 2021-11-24 19:52:23 -05:00
Thomas Pelletier 79e78b234c Decoder: fix panic on table array behind a pointer (#682)
Fixes #677
2021-11-24 18:50:04 -05:00
Thomas Pelletier 1b5a25c0ef Decoder: fail on unescaped \r not followed by \n (#681)
Fixes #674
2021-11-24 18:11:36 -05:00
Thomas Pelletier 8eae15b2ee Decoder: validate bounds of day and month in dates (#680)
Fixes #676
2021-11-24 17:42:01 -05:00
Thomas Pelletier 2b3de620e8 Encoder: try to use pointer type TextMarshaler (#679)
If a type does not implement the encoding.TextMarshaler interface but
its pointer type does, use it if possible.

Fixes #678
2021-11-24 14:43:49 -05:00
Cameron Moore 8645d6376b Decoder: flag invalid carriage returns in literal strings (#673) 2021-11-23 22:41:59 -05:00
Thomas Pelletier 64fe47161f API: Encoder and Decoder options are chainable (#670)
Fixes #583
2021-11-13 19:04:53 -05:00
Thomas Pelletier 4dff8eaa4d Decoder: prevent duplicates of inline tables (#667)
* seen: prevent duplicates of inline tables

* Provide clearer error message for redefined keys

For example:

``
toml: key b is already defined
```
2021-11-10 10:04:43 -05:00
Cameron Moore 2dbd29a565 parser: Fix missing check for upper exponent (#665) 2021-11-09 21:15:23 -05:00
Thomas Pelletier f27a07d31a seen: verify arrays (#663)
Fixes #662
2021-11-09 20:26:30 -05:00
Thomas Pelletier 644515958c Update TOML test suite (#661)
Ref #658
2021-11-08 22:35:35 -05:00
Thomas Pelletier 8683be35f6 seen: check inline tables (#660)
Fixes #658
2021-11-08 21:53:02 -05:00
Thomas Pelletier dc1740d473 Decode: code cleanup for struct cache (#659) 2021-11-07 18:35:30 -05:00
Thomas Pelletier 11f789ef11 Decode: prevent comments that look like dates to be accepted (#657)
* parser: fix date detection

When the parser has to decide between parsing and integer or a date, it should
check that all characters are actually acceptable (digits, or date/time
elements).

Fixes #655
2021-11-04 22:06:12 -04:00
Thomas Pelletier 74d21b367f scanner: handle carriage return in comments (#656)
Fixes #653
2021-11-04 21:40:16 -04:00
Thomas Pelletier 6617e7e73d utf8: use lookup table to validate ASCII (#654) 2021-11-04 16:05:36 -04:00
Thomas Pelletier 3dbca20bc9 Decoder: flag invalid carriage returns in strings (#652)
Fixes #651
2021-11-02 10:02:25 -04:00
Thomas Pelletier 85c0658984 Decode: add missing checks for LocalTime (#650) 2021-10-29 22:13:08 -04:00
Thomas Pelletier 772d169b52 testsuite: return error when can't encode tag (#648) 2021-10-29 21:51:50 -04:00
Cameron Moore b4ec220f7e Update tomltestgen and regenerate tests (#645)
Remove testsuite build tag from generated tests file

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-10-28 20:46:08 -04:00
Thomas Pelletier 3694ae88f6 decode: error on _ before exponent in floats (#647)
Fixes #646
2021-10-28 20:41:10 -04:00
80 changed files with 7865 additions and 2383 deletions
+1
View File
@@ -1,3 +1,4 @@
* text=auto
benchmark/benchmark.toml text eol=lf
testdata/** text eol=lf
+1
View File
@@ -2,6 +2,7 @@ changelog:
exclude:
labels:
- build
- testing
categories:
- title: What's new
labels:
+26
View File
@@ -0,0 +1,26 @@
name: CIFuzz
on: [pull_request]
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'go-toml'
dry-run: false
language: go
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'go-toml'
fuzz-seconds: 300
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
+4 -4
View File
@@ -35,11 +35,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3
+3 -3
View File
@@ -9,12 +9,12 @@ jobs:
runs-on: "ubuntu-latest"
name: report
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@master
uses: actions/setup-go@v5
with:
go-version: 1.16
go-version: "1.22"
- name: Run tests with coverage
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@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
distribution: goreleaser
version: latest
args: release ${{ inputs.args }} --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+11 -4
View File
@@ -11,15 +11,22 @@ jobs:
build:
strategy:
matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
go: [ '1.16', '1.17' ]
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest', 'macos-14' ]
go: [ '1.21', '1.22' ]
runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }}
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup go ${{ matrix.go }}
uses: actions/setup-go@master
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Run unit tests
run: go test -race ./...
release-check:
if: ${{ github.ref != 'refs/heads/v2' }}
uses: pelletier/go-toml/.github/workflows/release.yml@v2
with:
args: --snapshot
+2
View File
@@ -3,3 +3,5 @@ fuzz/
cmd/tomll/tomll
cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
dist
tests/
+126
View File
@@ -0,0 +1,126 @@
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
- linux_arm64
- linux_arm
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- 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
- linux_arm64
- linux_arm
- linux_riscv64
- windows_amd64
- windows_arm64
- windows_arm
- 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
- linux_arm64
- linux_riscv64
- linux_arm
- windows_amd64
- windows_arm64
- windows_arm
- 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
+20 -9
View File
@@ -155,6 +155,8 @@ Checklist:
- Does not introduce backward-incompatible changes (unless discussed).
- Has relevant doc changes.
- 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".
2. Make sure to edit the commit message to keep all the useful information
@@ -163,13 +165,22 @@ Checklist:
### New release
1. Go to [releases][releases]. Click on "X commits to master since this
release".
2. Make note of all the changes. Look for backward incompatible changes,
new features, and bug fixes.
3. Pick the new version using the above and semver.
4. Create a [new release][new-release].
5. Follow the same format as [1.1.0][release-110].
1. Decide on the next version number. Use semver. Review commits since last
version to assess.
2. Tag release. For example:
```
git checkout v2
git pull
git tag v2.2.0
git push --tags
```
3. CI automatically builds a draft Github release. Review it and edit as
necessary. 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. Check "create discussion" box, in the "Releases" category.
5. If new version is an alpha or beta only, check pre-release box.
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
@@ -177,6 +188,6 @@ Checklist:
[readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo
[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
[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
+2 -1
View File
@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
go-toml v2
Copyright (c) 2021 - 2023 Thomas Pelletier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+213 -64
View File
@@ -4,24 +4,14 @@ Go library for the [TOML](https://toml.io/en/) format.
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
## Development status
This is the upcoming major version of go-toml. It is currently in active
development. As of release v2.0.0-beta.1, the library has reached feature parity
with v1, and fixes a lot known bugs and performance issues along the way.
If you do not need the advanced document editing features of v1, you are
encouraged to try out this version.
[👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
## 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)
@@ -48,22 +38,22 @@ operations should not be shockingly slow. See [benchmarks](#benchmarks).
### Strict mode
`Decoder` can be set to "strict mode", which makes it error when some parts of
the TOML document was not prevent in the target structure. This is a great way
the TOML document was not present in the target structure. This is a great way
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
When decoding errors occur, go-toml returns [`DecodeError`][decode-err]), which
contains a human readable contextualized version of the error. For example:
When most decoding errors occur, go-toml returns [`DecodeError`][decode-err],
which contains a human readable contextualized version of the error. For
example:
```
2| key1 = "value1"
3| key2 = "missing2"
| ~~~~ missing field
4| key3 = "missing3"
5| key4 = "value4"
1| [server]
2| path = 100
| ~~~ cannot decode TOML integer into struct field toml_test.Server.Path of type string
3| port = 50
```
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
@@ -82,15 +72,35 @@ representation.
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
### Commented config
Since TOML is often used for configuration files, go-toml can emit documents
annotated with [comments and commented-out values][comments-example]. For
example, it can generate the following file:
```toml
# Host IP to connect to.
host = '127.0.0.1'
# Port of the remote server.
port = 4242
# Encryption parameters (optional)
# [TLS]
# cipher = 'AEAD-AES128-GCM-SHA256'
# version = 'TLS 1.3'
```
[comments-example]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Marshal-Commented
## Getting started
Given the following struct, let's see how to read it and write it as TOML:
```go
type MyConfig struct {
Version int
Name string
Tags []string
Version int
Name string
Tags []string
}
```
@@ -109,7 +119,7 @@ tags = ["go", "toml"]
var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
panic(err)
}
fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name)
@@ -130,14 +140,14 @@ as a TOML document:
```go
cfg := MyConfig{
Version: 2,
Name: "go-toml",
Tags: []string{"go", "toml"},
Version: 2,
Name: "go-toml",
Tags: []string{"go", "toml"},
}
b, err := toml.Marshal(cfg)
if err != nil {
panic(err)
panic(err)
}
fmt.Println(string(b))
@@ -149,22 +159,33 @@ fmt.Println(string(b))
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
## Unstable API
This API does not yet follow the backward compatibility guarantees of this
library. They provide early access to features that may have rough edges or an
API subject to change.
### Parser
Parser is the unstable API that allows iterative parsing of a TOML document at
the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
## Benchmarks
Execution time speedup compared to other Go TOML libraries:
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<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/struct-2</td><td>2.4x</td><td>2.6x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.7x</td><td>2.6x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.1x</td></tr>
</tbody>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
<p>The table above has the results of the most common use-cases. The table below
@@ -172,22 +193,22 @@ contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>2.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.1x</td><td>3.1x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.4x</td><td>4.3x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.4x</td><td>3.2x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.2x</td><td>2.5x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.8x</td><td>2.7x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.0x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.4x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.4x</td><td>2.9x</td></tr>
<tr><td>[Geo mean]</td><td>2.8x</td><td>2.6x</td></tr>
</tbody>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details>
@@ -206,6 +227,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
## 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 available on [ghcr.io][docker].
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
## Migrating from v1
This section describes the differences between v1 and v2, with some pointers on
@@ -234,16 +293,16 @@ element in the interface to decode the object. For example:
```go
type inner struct {
B interface{}
B interface{}
}
type doc struct {
A interface{}
A interface{}
}
d := doc{
A: inner{
B: "Before",
},
A: inner{
B: "Before",
},
}
data := `
@@ -282,7 +341,7 @@ contained in the doc is superior to the capacity of the array. For example:
```go
type doc struct {
A [2]string
A [2]string
}
d := doc{}
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
@@ -324,6 +383,29 @@ The recommended replacement is pre-filling the struct before unmarshaling.
[go-defaults]: https://github.com/mcuadros/go-defaults
#### `toml.Tree` replacement
This structure was the initial attempt at providing a document model for
go-toml. It allows manipulating the structure of any document, encoding and
decoding from their TOML representation. While a more robust feature was
initially planned in go-toml v2, this has been ultimately [removed from
scope][nodoc] of this library, with no plan to add it back at the moment. The
closest equivalent at the moment would be to unmarshal into an `interface{}` and
use type assertions and/or reflection to manipulate the arbitrary
structure. However this would fall short of providing all of the TOML features
such as adding comments and be specific about whitespace.
#### `toml.Position` are not retrievable anymore
The API for retrieving the position (line, column) of a specific TOML element do
not exist anymore. This was done to minimize the amount of concepts introduced
by the library (query path), and avoid the performance hit related to storing
positions in the absence of a document model, for a feature that seemed to have
little use. Errors however have gained more detailed position
information. Position retrieval seems better fitted for a document model, which
has been [removed from the scope][nodoc] of go-toml v2 at the moment.
### Encoding / Marshal
#### Default struct fields order
@@ -359,7 +441,8 @@ fmt.Println("v2:\n" + string(b))
```
There is no way to make v2 encoder behave like v1. A workaround could be to
manually sort the fields alphabetically in the struct definition.
manually sort the fields alphabetically in the struct definition, or generate
struct types using `reflect.StructOf`.
#### No indentation by default
@@ -407,7 +490,9 @@ fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
V1 always uses double quotes (`"`) around strings and keys that cannot be
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
unless a character cannot be represented, then falls back to double quotes.
unless a character cannot be represented, then falls back to double quotes. As a
result of this change, `Encoder.QuoteMapKeys` has been removed, as it is not
useful anymore.
There is no way to make v2 encoder behave like v1.
@@ -422,6 +507,70 @@ There is no way to make v2 encoder behave like v1.
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
#### `Encoder.CompactComments` has been removed
Emitting compact comments is now the default behavior of go-toml. This option
is not necessary anymore.
#### Struct tags have been merged
V1 used to provide multiple struct tags: `comment`, `commented`, `multiline`,
`toml`, and `omitempty`. To behave more like the standard library, v2 has merged
`toml`, `multiline`, `commented`, and `omitempty`. For example:
```go
type doc struct {
// v1
F string `toml:"field" multiline:"true" omitempty:"true" commented:"true"`
// v2
F string `toml:"field,multiline,omitempty,commented"`
}
```
Has a result, the `Encoder.SetTag*` methods have been removed, as there is just
one tag now.
#### `Encoder.ArraysWithOneElementPerLine` has been renamed
The new name is `Encoder.SetArraysMultiline`. The behavior should be the same.
#### `Encoder.Indentation` has been renamed
The new name is `Encoder.SetIndentSymbol`. The behavior should be the same.
#### Embedded structs behave like stdlib
V1 defaults to merging embedded struct fields into the embedding struct. This
behavior was unexpected because it does not follow the standard library. To
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
added to make the encoder behave correctly. Given backward compatibility is not
a problem anymore, v2 does the right thing by default: it follows the behavior
of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.
[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
## Versioning
Expect for parts explicitely marked otherwise, go-toml follows [Semantic
Versioning](https://semver.org). The supported version of
[TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this
document. The last two major versions of Go are supported (see [Go Release
Policy](https://golang.org/doc/devel/release.html#policy)).
## License
The MIT License (MIT). Read [LICENSE](LICENSE).
-3
View File
@@ -2,9 +2,6 @@
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ---------- | ------------------ |
| Latest 2.x | :white_check_mark: |
+26 -13
View File
@@ -76,8 +76,10 @@ cover() {
fi
pushd "$dir"
go test -covermode=atomic -coverprofile=coverage.out ./...
go test -covermode=atomic -coverpkg=./... -coverprofile=coverage.out.tmp ./...
grep -Ev '(fuzz|testsuite|tomltestgen|gotoml-test-decoder|gotoml-test-encoder)' coverage.out.tmp > coverage.out
go tool cover -func=coverage.out
echo "Coverage profile for ${branch}: ${dir}/coverage.out" >&2
popd
if [ "${branch}" != "HEAD" ]; then
@@ -103,16 +105,23 @@ coverage() {
echo ""
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
head_pct="$(cat ${head_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="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')"
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
echo "Delta: ${delta_pct}"
if [[ $delta_pct = \-* ]]; then
echo "Regression!";
return 1
echo "Regression!";
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
return 0
;;
@@ -143,7 +152,7 @@ bench() {
fi
export GOMAXPROCS=2
nice -n -19 taskset --cpu-list 0,1 go test '-bench=^Benchmark(Un)?[mM]arshal' -count=5 -run=Nothing ./... | tee "${out}"
go test '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}"
popd
if [ "${branch}" != "HEAD" ]; then
@@ -152,10 +161,12 @@ bench() {
}
fmktemp() {
if mktemp --version|grep GNU >/dev/null; then
mktemp --suffix=-$1;
if mktemp --version &> /dev/null; then
# GNU
mktemp --suffix=-$1
else
mktemp -t $1;
# BSD
mktemp -t $1
fi
}
@@ -175,12 +186,14 @@ with open(sys.argv[1]) as f:
lines.append(line.split(','))
results = []
for line in reversed(lines[1:]):
for line in reversed(lines[2:]):
if len(line) < 8 or line[0] == "":
continue
v2 = float(line[1])
results.append([
line[0].replace("-32", ""),
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[5])/v2), # bs
"%.1fx" % (float(line[7])/v2), # bs
])
# move geomean to the end
results.append(results[0])
@@ -251,10 +264,10 @@ benchmark() {
if [ "$1" = "-html" ]; then
tmpcsv=`fmktemp csv`
benchstat -csv -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
benchstat -format csv go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
benchstathtml $tmpcsv
else
benchstat -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt
benchstat go-toml-v2.txt go-toml-v1.txt bs-toml.txt
fi
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"os"
"path"
"github.com/pelletier/go-toml/v2/testsuite"
"github.com/pelletier/go-toml/v2/internal/testsuite"
)
func main() {
@@ -0,0 +1,30 @@
package main
import (
"flag"
"log"
"os"
"path"
"github.com/pelletier/go-toml/v2/internal/testsuite"
)
func main() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
if flag.NArg() != 0 {
flag.Usage()
}
err := testsuite.EncodeStdin()
if err != nil {
log.Fatal(err)
}
}
func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
+66
View File
@@ -0,0 +1,66 @@
// 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"
"flag"
"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
`
var useJsonNumber bool
func main() {
flag.BoolVar(&useJsonNumber, "use-json-number", false, "unmarshal numbers into `json.Number` type instead of as `float64`")
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)
e := toml.NewEncoder(w)
if useJsonNumber {
d.UseNumber()
e.SetMarshalJsonNumbers(true)
}
err := d.Decode(&v)
if err != nil {
return err
}
return e.Encode(v)
}
+63
View File
@@ -0,0 +1,63 @@
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
useJsonNumber bool
}{
{
name: "valid json",
input: `
{
"mytoml": {
"a": 42
}
}`,
expected: `[mytoml]
a = 42.0
`,
},
{
name: "use json number",
useJsonNumber: true,
input: `
{
"mytoml": {
"a": 42
}
}`,
expected: `[mytoml]
a = 42
`,
},
{
name: "invalid json",
input: `{ foo`,
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
useJsonNumber = e.useJsonNumber
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)
}
+45
View File
@@ -0,0 +1,45 @@
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())
}
}
}
+45 -91
View File
@@ -3,21 +3,17 @@
//
// Within the go-toml package, run `go generate`. Otherwise, use:
//
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"go/format"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"path/filepath"
"strconv"
"strings"
"text/template"
@@ -43,8 +39,7 @@ type testsCollection struct {
Count int
}
const srcTemplate = "// +build testsuite\n\n" +
"// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
"package toml_test\n" +
" import (\n" +
" \"testing\"\n" +
@@ -65,30 +60,6 @@ const srcTemplate = "// +build testsuite\n\n" +
"}\n" +
"{{end}}\n"
func downloadTmpFile(url string) string {
log.Println("starting to download file from", url)
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
if err != nil {
panic(err)
}
defer tmpfile.Close()
copiedLen, err := io.Copy(tmpfile, resp.Body)
if err != nil {
panic(err)
}
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
}
return tmpfile.Name()
}
func kebabToCamel(kebab string) string {
camel := ""
nextUpper := true
@@ -108,19 +79,6 @@ func kebabToCamel(kebab string) string {
return camel
}
func readFileFromZip(f *zip.File) string {
reader, err := f.Open()
if err != nil {
panic(err)
}
defer reader.Close()
bytes, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return string(bytes)
}
func templateGoStr(input string) string {
return strconv.Quote(input)
}
@@ -139,61 +97,57 @@ func main() {
flag.Usage = usage
flag.Parse()
url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
resultFile := downloadTmpFile(url)
defer os.Remove(resultFile)
log.Println("file written to", resultFile)
zipReader, err := zip.OpenReader(resultFile)
if err != nil {
panic(err)
}
defer zipReader.Close()
collection := testsCollection{
Ref: *ref,
Timestamp: time.Now().Format(time.RFC3339),
}
zipFilesMap := map[string]*zip.File{}
dirContent, _ := filepath.Glob("tests/invalid/**/*.toml")
for _, f := range dirContent {
filename := strings.TrimPrefix(f, "tests/valid/")
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
for _, f := range zipReader.File {
zipFilesMap[f.Name] = f
log.Printf("> [%s] %s\n", "invalid", name)
tomlContent, err := os.ReadFile(f)
if err != nil {
fmt.Printf("failed to read test file: %s\n", err)
os.Exit(1)
}
collection.Invalid = append(collection.Invalid, invalid{
Name: name,
Input: string(tomlContent),
})
collection.Count++
}
testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
for _, f := range zipReader.File {
groups := testFileRegexp.FindStringSubmatch(f.Name)
if len(groups) > 0 {
name := kebabToCamel(groups[3])
testType := groups[2]
dirContent, _ = filepath.Glob("tests/valid/**/*.toml")
for _, f := range dirContent {
filename := strings.TrimPrefix(f, "tests/valid/")
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
log.Printf("> [%s] %s\n", testType, name)
log.Printf("> [%s] %s\n", "valid", name)
tomlContent := readFileFromZip(f)
switch testType {
case "invalid":
collection.Invalid = append(collection.Invalid, invalid{
Name: name,
Input: tomlContent,
})
collection.Count++
case "valid":
baseFilePath := groups[1]
jsonFilePath := baseFilePath + ".json"
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])
collection.Valid = append(collection.Valid, valid{
Name: name,
Input: tomlContent,
JsonRef: jsonContent,
})
collection.Count++
default:
panic(fmt.Sprintf("unknown test type: %s", testType))
}
tomlContent, err := os.ReadFile(f)
if err != nil {
fmt.Printf("failed reading test file: %s\n", err)
os.Exit(1)
}
filename = strings.TrimSuffix(f, ".toml")
jsonContent, err := os.ReadFile(filename + ".json")
if err != nil {
fmt.Printf("failed reading validation json: %s\n", err)
os.Exit(1)
}
collection.Valid = append(collection.Valid, valid{
Name: name,
Input: string(tomlContent),
JsonRef: string(jsonContent),
})
collection.Count++
}
log.Printf("Collected %d tests from toml-test\n", collection.Count)
@@ -203,7 +157,7 @@ func main() {
}
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
buf := new(bytes.Buffer)
err = t.Execute(buf, collection)
err := t.Execute(buf, collection)
if err != nil {
panic(err)
}
@@ -217,7 +171,7 @@ func main() {
return
}
err = os.WriteFile(*out, outputBytes, 0644)
err = os.WriteFile(*out, outputBytes, 0o644)
if err != nil {
panic(err)
}
+151 -65
View File
@@ -5,6 +5,8 @@ import (
"math"
"strconv"
"time"
"github.com/pelletier/go-toml/v2/unstable"
)
func parseInteger(b []byte) (int64, error) {
@@ -32,33 +34,45 @@ func parseLocalDate(b []byte) (LocalDate, error) {
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
return date, unstable.NewParserError(b, "dates are expected to have the format YYYY-MM-DD")
}
date.Year = parseDecimalDigits(b[0:4])
var err error
v := parseDecimalDigits(b[5:7])
date.Year, err = parseDecimalDigits(b[0:4])
if err != nil {
return LocalDate{}, err
}
date.Month = v
date.Month, err = parseDecimalDigits(b[5:7])
if err != nil {
return LocalDate{}, err
}
date.Day = parseDecimalDigits(b[8:10])
date.Day, err = parseDecimalDigits(b[8:10])
if err != nil {
return LocalDate{}, err
}
if !isValidDate(date.Year, date.Month, date.Day) {
return LocalDate{}, newDecodeError(b, "impossible date")
return LocalDate{}, unstable.NewParserError(b, "impossible date")
}
return date, nil
}
func parseDecimalDigits(b []byte) int {
func parseDecimalDigits(b []byte) (int, error) {
v := 0
for _, c := range b {
for i, c := range b {
if c < '0' || c > '9' {
return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)")
}
v *= 10
v += int(c - '0')
}
return v
return v, nil
}
func parseDateTime(b []byte) (time.Time, error) {
@@ -85,22 +99,49 @@ func parseDateTime(b []byte) (time.Time, error) {
} else {
const dateTimeByteLen = 6
if len(b) != dateTimeByteLen {
return time.Time{}, newDecodeError(b, "invalid date-time timezone")
return time.Time{}, unstable.NewParserError(b, "invalid date-time timezone")
}
direction := 1
if b[0] == '-' {
var direction int
switch b[0] {
case '-':
direction = -1
case '+':
direction = +1
default:
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character")
}
if b[3] != ':' {
return time.Time{}, unstable.NewParserError(b[3:4], "expected a : separator")
}
hours, err := parseDecimalDigits(b[1:3])
if err != nil {
return time.Time{}, err
}
if hours > 23 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset hours")
}
minutes, err := parseDecimalDigits(b[4:6])
if err != nil {
return time.Time{}, err
}
if minutes > 59 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset minutes")
}
hours := digitsToInt(b[1:3])
minutes := digitsToInt(b[4:6])
seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds)
if seconds == 0 {
zone = time.UTC
} else {
zone = time.FixedZone("", seconds)
}
b = b[dateTimeByteLen:]
}
if len(b) > 0 {
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
}
t := time.Date(
@@ -121,7 +162,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen {
return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
}
date, err := parseLocalDate(b[:10])
@@ -132,7 +173,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
sep := b[10]
if sep != 'T' && sep != ' ' && sep != 't' {
return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space")
}
t, rest, err := parseLocalTime(b[11:])
@@ -156,61 +197,86 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
const localTimeByteLen = 8
if len(b) < localTimeByteLen {
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
}
t.Hour = parseDecimalDigits(b[0:2])
if t.Hour > 23 {
return t, nil, newDecodeError(b[0:2], "hour cannot be greater 23")
return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
}
if b[2] != ':' {
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes")
}
t.Minute = parseDecimalDigits(b[3:5])
t.Minute, err = parseDecimalDigits(b[3:5])
if err != nil {
return t, nil, err
}
if t.Minute > 59 {
return t, nil, newDecodeError(b[3:5], "minutes cannot be greater 59")
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
}
if b[5] != ':' {
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
}
t.Second = parseDecimalDigits(b[6:8])
if t.Second > 59 {
return t, nil, newDecodeError(b[3:5], "seconds cannot be greater 59")
t.Second, err = parseDecimalDigits(b[6:8])
if err != nil {
return t, nil, err
}
const minLengthWithFrac = 9
if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' {
if t.Second > 60 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater 60")
}
b = b[8:]
if len(b) >= 1 && b[0] == '.' {
frac := 0
precision := 0
digits := 0
for i, c := range b[minLengthWithFrac:] {
for i, c := range b[1:] {
if !isDigit(c) {
if i == 0 {
return t, nil, newDecodeError(b[i:i+1], "need at least one digit after fraction point")
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
}
break
}
digits++
const maxFracPrecision = 9
if i >= maxFracPrecision {
return t, nil, newDecodeError(b[i:i+1], "maximum precision for date time is nanosecond")
// go-toml allows decoding fractional seconds
// beyond the supported precision of 9
// digits. It truncates the fractional component
// to the supported precision and ignores the
// remaining digits.
//
// https://github.com/pelletier/go-toml/discussions/707
continue
}
frac *= 10
frac += int(c - '0')
digits++
precision++
}
t.Nanosecond = frac * nspow[digits]
t.Precision = digits
if precision == 0 {
return t, nil, unstable.NewParserError(b[:1], "nanoseconds need at least one digit")
}
return t, b[9+digits:], nil
t.Nanosecond = frac * nspow[precision]
t.Precision = precision
return t, b[1+digits:], nil
}
return t, b[8:], nil
return t, b, nil
}
//nolint:cyclop
@@ -225,40 +291,40 @@ func parseFloat(b []byte) (float64, error) {
}
if cleaned[0] == '.' {
return 0, newDecodeError(b, "float cannot start with a dot")
return 0, unstable.NewParserError(b, "float cannot start with a dot")
}
if cleaned[len(cleaned)-1] == '.' {
return 0, newDecodeError(b, "float cannot end with a dot")
return 0, unstable.NewParserError(b, "float cannot end with a dot")
}
dotAlreadySeen := false
for i, c := range cleaned {
if c == '.' {
if dotAlreadySeen {
return 0, newDecodeError(b[i:i+1], "float can have at most one decimal point")
return 0, unstable.NewParserError(b[i:i+1], "float can have at most one decimal point")
}
if !isDigit(cleaned[i-1]) {
return 0, newDecodeError(b[i-1:i+1], "float decimal point must be preceded by a digit")
return 0, unstable.NewParserError(b[i-1:i+1], "float decimal point must be preceded by a digit")
}
if !isDigit(cleaned[i+1]) {
return 0, newDecodeError(b[i:i+2], "float decimal point must be followed by a digit")
return 0, unstable.NewParserError(b[i:i+2], "float decimal point must be followed by a digit")
}
dotAlreadySeen = true
}
}
start := 0
if b[0] == '+' || b[0] == '-' {
if cleaned[0] == '+' || cleaned[0] == '-' {
start = 1
}
if b[start] == '0' && isDigit(b[start+1]) {
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
if cleaned[start] == '0' && len(cleaned) > start+1 && isDigit(cleaned[start+1]) {
return 0, unstable.NewParserError(b, "float integer part cannot have leading zeroes")
}
f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil {
return 0, newDecodeError(b, "unable to parse float: %w", err)
return 0, unstable.NewParserError(b, "unable to parse float: %w", err)
}
return f, nil
@@ -272,7 +338,7 @@ func parseIntHex(b []byte) (int64, error) {
i, err := strconv.ParseInt(string(cleaned), 16, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
return 0, unstable.NewParserError(b, "couldn't parse hexadecimal number: %w", err)
}
return i, nil
@@ -286,7 +352,7 @@ func parseIntOct(b []byte) (int64, error) {
i, err := strconv.ParseInt(string(cleaned), 8, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
return 0, unstable.NewParserError(b, "couldn't parse octal number: %w", err)
}
return i, nil
@@ -300,7 +366,7 @@ func parseIntBin(b []byte) (int64, error) {
i, err := strconv.ParseInt(string(cleaned), 2, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
return 0, unstable.NewParserError(b, "couldn't parse binary number: %w", err)
}
return i, nil
@@ -323,24 +389,33 @@ func parseIntDec(b []byte) (int64, error) {
}
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
return 0, newDecodeError(b, "leading zero not allowed on decimal number")
return 0, unstable.NewParserError(b, "leading zero not allowed on decimal number")
}
i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
return 0, unstable.NewParserError(b, "couldn't parse decimal number: %w", err)
}
return i, nil
}
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
if b[0] == '_' {
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
start := 0
if b[start] == '+' || b[start] == '-' {
start++
}
if len(b) == start {
return b, nil
}
if b[start] == '_' {
return nil, unstable.NewParserError(b[start:start+1], "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
}
// fast path
@@ -362,7 +437,7 @@ func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
c := b[i]
if c == '_' {
if !before {
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
}
before = false
} else {
@@ -376,11 +451,11 @@ func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
if b[0] == '_' {
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
return nil, unstable.NewParserError(b[0:1], "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
}
// fast path
@@ -403,20 +478,27 @@ func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
switch c {
case '_':
if !before {
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
}
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore before exponent")
}
before = false
case '+', '-':
// signed exponents
cleaned = append(cleaned, c)
before = false
case 'e', 'E':
if i < len(b)-1 && b[i+1] == '_' {
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after exponent")
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after exponent")
}
cleaned = append(cleaned, c)
case '.':
if i < len(b)-1 && b[i+1] == '_' {
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after decimal point")
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after decimal point")
}
if i > 0 && b[i-1] == '_' {
return nil, newDecodeError(b[i-1:i], "cannot have underscore before decimal point")
return nil, unstable.NewParserError(b[i-1:i], "cannot have underscore before decimal point")
}
cleaned = append(cleaned, c)
default:
@@ -430,7 +512,7 @@ func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
// isValidDate checks if a provided date is a date that exists.
func isValidDate(year int, month int, day int) bool {
return day <= daysIn(month, year)
return month > 0 && month < 13 && day > 0 && day <= daysIn(month, year)
}
// daysBefore[m] counts the number of days in a non-leap year
@@ -462,3 +544,7 @@ func daysIn(m int, year int) int {
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
func isDigit(r byte) bool {
return r >= '0' && r <= '9'
}
+9 -26
View File
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/unstable"
)
// DecodeError represents an error encountered during the parsing or decoding
@@ -27,7 +28,7 @@ type DecodeError struct {
// corresponding field in the target value. It contains all the missing fields
// in Errors.
//
// Emitted by Decoder when SetStrict(true) was called.
// Emitted by Decoder when DisallowUnknownFields() was called.
type StrictMissingError struct {
// One error per field that could not be found.
Errors []DecodeError
@@ -55,25 +56,6 @@ func (s *StrictMissingError) String() string {
type Key []string
// internal version of DecodeError that is used as the base to create a
// DecodeError with full context.
type decodeError struct {
highlight []byte
message string
key Key // optional
}
func (de *decodeError) Error() string {
return de.message
}
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
return &decodeError{
highlight: highlight,
message: fmt.Errorf(format, args...).Error(),
}
}
// Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string {
return "toml: " + e.message
@@ -103,13 +85,14 @@ func (e *DecodeError) Key() Key {
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//
//nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
offset := danger.SubsliceOffset(document, de.highlight)
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := danger.SubsliceOffset(document, de.Highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
before, after := linesOfContext(document, de.highlight, offset, 3)
before, after := linesOfContext(document, de.Highlight, offset, 3)
var buf strings.Builder
@@ -139,7 +122,7 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
buf.Write(before[0])
}
buf.Write(de.highlight)
buf.Write(de.Highlight)
if len(after) > 0 {
buf.Write(after[0])
@@ -157,7 +140,7 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
buf.WriteString(strings.Repeat(" ", len(before[0])))
}
buf.WriteString(strings.Repeat("~", len(de.highlight)))
buf.WriteString(strings.Repeat("~", len(de.Highlight)))
if len(errMessage) > 0 {
buf.WriteString(" ")
@@ -182,7 +165,7 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
message: errMessage,
line: errLine,
column: errColumn,
key: de.key,
key: de.Key,
human: buf.String(),
}
}
+10 -9
View File
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/pelletier/go-toml/v2/unstable"
"github.com/stretchr/testify/assert"
)
@@ -170,9 +171,9 @@ line 5`,
doc := b.Bytes()
hl := doc[start:end]
err := wrapDecodeError(doc, &decodeError{
highlight: hl,
message: e.msg,
err := wrapDecodeError(doc, &unstable.ParserError{
Highlight: hl,
Message: e.msg,
})
var derr *DecodeError
@@ -212,12 +213,12 @@ func ExampleDecodeError() {
fmt.Println(err)
//nolint:errorlint
de := err.(*DecodeError)
fmt.Println(de.String())
row, col := de.Position()
fmt.Println("error occurred at row", row, "column", col)
var derr *DecodeError
if errors.As(err, &derr) {
fmt.Println(derr.String())
row, col := derr.Position()
fmt.Println("error occurred at row", row, "column", col)
}
// Output:
// toml: number must have at least one digit between underscores
// 1| name = 123__456
+37
View File
@@ -0,0 +1,37 @@
package toml_test
import (
"fmt"
"log"
"strconv"
"github.com/pelletier/go-toml/v2"
)
type customInt int
func (i *customInt) UnmarshalText(b []byte) error {
x, err := strconv.ParseInt(string(b), 10, 32)
if err != nil {
return err
}
*i = customInt(x * 100)
return nil
}
type doc struct {
Value customInt
}
func ExampleUnmarshal_textUnmarshal() {
var x doc
data := []byte(`value = "42"`)
err := toml.Unmarshal(data, &x)
if err != nil {
log.Fatal(err)
}
fmt.Println(x)
// Output:
// {4200}
}
+8 -1
View File
@@ -7,13 +7,20 @@ import (
"github.com/stretchr/testify/require"
)
func TestFastSimple(t *testing.T) {
func TestFastSimpleInt(t *testing.T) {
m := map[string]int64{}
err := toml.Unmarshal([]byte(`a = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]int64{"a": 42}, m)
}
func TestFastSimpleFloat(t *testing.T) {
m := map[string]float64{}
err := toml.Unmarshal([]byte("a = 42\nb = 1.1\nc = 12341234123412341234123412341234"), &m)
require.NoError(t, err)
require.Equal(t, map[string]float64{"a": 42, "b": 1.1, "c": 1.2341234123412342e+31}, m)
}
func TestFastSimpleString(t *testing.T) {
m := map[string]string{}
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
+56
View File
@@ -0,0 +1,56 @@
//go:build go1.18 || go1.19 || go1.20 || go1.21 || go1.22
// +build go1.18 go1.19 go1.20 go1.21 go1.22
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
// latest (v1.7.0) doesn't have the fix for time.Time
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
require github.com/stretchr/testify v1.9.0
+12 -4
View File
@@ -1,11 +1,19 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-152
View File
@@ -1,152 +0,0 @@
package ast
import (
"fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// Iterator starts uninitialized, you need to call Next() first.
//
// For example:
//
// it := n.Children()
// for it.Next() {
// it.Node()
// }
type Iterator struct {
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a node, false
// otherwise.
func (c *Iterator) Next() bool {
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
}
return c.node.Valid()
}
// IsLast returns true if the current node of the iterator is the last one.
// Subsequent call to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next == 0
}
// Node returns a copy of the node pointed at by the iterator.
func (c *Iterator) Node() *Node {
return c.node
}
// Root contains a full AST.
//
// It is immutable once constructed with Builder.
type Root struct {
nodes []Node
}
// Iterator over the top level nodes.
func (r *Root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = &r.nodes[0]
}
return it
}
func (r *Root) at(idx Reference) *Node {
return &r.nodes[idx]
}
// Arrays have one child per element in the array.
// InlineTables have one child per key-value pair in the table.
// KeyValues have at least two children. The first one is the value. The
// rest make a potentially dotted key.
// Table and Array table have one child per element of the key they
// represent (same as KeyValue, but without the last node being the value).
// children []Node
type Node struct {
Kind Kind
Raw Range // Raw bytes from the input.
Data []byte // Node value (could be either allocated or referencing the input).
// References to other nodes, as offsets in the backing array from this
// node. References can go backward, so those can be negative.
next int // 0 if last element
child int // 0 if no child
}
type Range struct {
Offset uint32
Length uint32
}
// Next returns a copy of the next node, or an invalid Node if there is no
// next node.
func (n *Node) Next() *Node {
if n.next == 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.next))
}
// Child returns a copy of the first child node of this node. Other children
// can be accessed calling Next on the first child.
// Returns an invalid Node if there is none.
func (n *Node) Child() *Node {
if n.child == 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.child))
}
// Valid returns true if the node's kind is set (not to Invalid).
func (n *Node) Valid() bool {
return n != nil
}
// Key returns the child nodes making the Key on a supported node. Panics
// otherwise.
// They are guaranteed to be all be of the Kind Key. A simple key would return
// just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(fmt.Errorf("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil.
// Panics if not called on a KeyValue node, or if the Children are malformed.
func (n *Node) Value() *Node {
assertKind(KeyValue, *n)
return n.Child()
}
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
}
func assertKind(k Kind, n Node) {
if n.Kind != k {
panic(fmt.Errorf("method was expecting a %s, not a %s", k, n.Kind))
}
}
-51
View File
@@ -1,51 +0,0 @@
package ast
type Reference int
const InvalidReference Reference = -1
func (r Reference) Valid() bool {
return r != InvalidReference
}
type Builder struct {
tree Root
lastIdx int
}
func (b *Builder) Tree() *Root {
return &b.tree
}
func (b *Builder) NodeAt(ref Reference) *Node {
return b.tree.at(ref)
}
func (b *Builder) Reset() {
b.tree.nodes = b.tree.nodes[:0]
b.lastIdx = 0
}
func (b *Builder) Push(n Node) Reference {
b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
return Reference(b.lastIdx)
}
func (b *Builder) PushAndChain(n Node) Reference {
newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
}
b.lastIdx = newIdx
return Reference(b.lastIdx)
}
func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent].child = int(child) - int(parent)
}
func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from].next = int(to) - int(from)
}
+42
View File
@@ -0,0 +1,42 @@
package characters
var invalidAsciiTable = [256]bool{
0x00: true,
0x01: true,
0x02: true,
0x03: true,
0x04: true,
0x05: true,
0x06: true,
0x07: true,
0x08: true,
// 0x09 TAB
// 0x0A LF
0x0B: true,
0x0C: true,
// 0x0D CR
0x0E: true,
0x0F: true,
0x10: true,
0x11: true,
0x12: true,
0x13: true,
0x14: true,
0x15: true,
0x16: true,
0x17: true,
0x18: true,
0x19: true,
0x1A: true,
0x1B: true,
0x1C: true,
0x1D: true,
0x1E: true,
0x1F: true,
// 0x20 - 0x7E Printable ASCII characters
0x7F: true,
}
func InvalidAscii(b byte) bool {
return invalidAsciiTable[b]
}
+6 -10
View File
@@ -1,4 +1,4 @@
package toml
package characters
import (
"unicode/utf8"
@@ -32,7 +32,7 @@ func (u utf8Err) Zero() bool {
// 0x9 => tab, ok
// 0xA - 0x1F => invalid
// 0x7F => invalid
func utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
offset := 0
for len(p) >= 8 {
@@ -48,7 +48,7 @@ func utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
}
for i, b := range p[:8] {
if invalidAscii(b) {
if InvalidAscii(b) {
err.Index = offset + i
err.Size = 1
return
@@ -62,7 +62,7 @@ func utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
for i := 0; i < n; {
pi := p[i]
if pi < utf8.RuneSelf {
if invalidAscii(pi) {
if InvalidAscii(pi) {
err.Index = offset + i
err.Size = 1
return
@@ -106,11 +106,11 @@ func utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
}
// Return the size of the next rune if valid, 0 otherwise.
func utf8ValidNext(p []byte) int {
func Utf8ValidNext(p []byte) int {
c := p[0]
if c < utf8.RuneSelf {
if invalidAscii(c) {
if InvalidAscii(c) {
return 0
}
return 1
@@ -140,10 +140,6 @@ func utf8ValidNext(p []byte) int {
return size
}
func invalidAscii(b byte) bool {
return b <= 0x08 || (b > 0x0A && b < 0x0D) || (b > 0x0D && b <= 0x1F) || b == 0x7F
}
// acceptRange gives the range of valid values for the second byte in a UTF-8
// sequence.
type acceptRange struct {
+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 conversion 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
}
+23
View File
@@ -0,0 +1,23 @@
package danger
import (
"reflect"
"unsafe"
)
// typeID is used as key in encoder and decoder caches to enable using
// the optimize runtime.mapaccess2_fast64 function instead of the more
// expensive lookup if we were to use reflect.Type as map key.
//
// typeID holds the pointer to the reflect.Type value, which is unique
// in the program.
//
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
type TypeID unsafe.Pointer
func MakeTypeID(t reflect.Type) TypeID {
// reflect.Type has the fields:
// typ unsafe.Pointer
// ptr unsafe.Pointer
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
}
@@ -67,6 +67,7 @@ func TestDocMarshal(t *testing.T) {
}
marshalTestToml := `title = 'TOML Marshal Testing'
[basic_lists]
floats = [12.3, 45.6, 78.9]
bools = [true, false, true]
@@ -89,7 +90,6 @@ name = 'Second'
[subdoc.first]
name = 'First'
[basic]
uint = 5001
bool = true
@@ -101,9 +101,9 @@ date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = 'List.First'
[[subdoclist]]
name = 'List.Second'
`
result, err := toml.Marshal(docData)
@@ -117,14 +117,15 @@ func TestBasicMarshalQuotedKey(t *testing.T) {
expected := `'Z.string-àéù' = 'Hello'
'Yfloat-𝟘' = 3.5
['Xsubdoc-àéù']
String2 = 'One'
[['W.sublist-𝟘']]
String2 = 'Two'
[['W.sublist-𝟘']]
String2 = 'Three'
`
require.Equal(t, string(expected), string(result))
@@ -159,8 +160,8 @@ bool = false
int = 0
string = ''
stringlist = []
[map]
[map]
`
require.Equal(t, string(expected), string(result))
@@ -151,6 +151,7 @@ type quotedKeyMarshalTestStruct struct {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{
String: "Hello",
@@ -160,6 +161,7 @@ var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var quotedKeyMarshalTestToml = []byte(`"Yfloat-𝟘" = 3.5
"Z.string-àéù" = "Hello"
@@ -272,6 +274,7 @@ var docData = testDoc{
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var mapTestDoc = testMapDoc{
Title: "TOML Marshal Testing",
@@ -457,35 +460,6 @@ func TestEmptytomlUnmarshal(t *testing.T) {
assert.Equal(t, emptyTestData, result)
}
func TestEmptyUnmarshalOmit(t *testing.T) {
t.Skipf("Have not figured yet if omitempty is a good idea")
type emptyMarshalTestStruct2 struct {
Title string `toml:"title"`
Bool bool `toml:"bool,omitempty"`
Int int `toml:"int, omitempty"`
String string `toml:"string,omitempty "`
StringList []string `toml:"stringlist,omitempty"`
Ptr *basicMarshalTestStruct `toml:"ptr,omitempty"`
Map map[string]string `toml:"map,omitempty"`
}
emptyTestData2 := emptyMarshalTestStruct2{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
result := emptyMarshalTestStruct2{}
err := toml.Unmarshal(emptyTestToml, &result)
require.NoError(t, err)
assert.Equal(t, emptyTestData2, result)
}
type pointerMarshalTestStruct struct {
Str *string
List *[]string
@@ -588,10 +562,12 @@ func (c customMarshaler) MarshalTOML() ([]byte, error) {
var customMarshalerData = customMarshaler{FirstName: "Sally", LastName: "Fields"}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var customMarshalerToml = []byte(`Sally Fields`)
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var nestedCustomMarshalerData = customMarshalerParent{
Self: customMarshaler{FirstName: "Maiku", LastName: "Suteda"},
@@ -599,6 +575,7 @@ var nestedCustomMarshalerData = customMarshalerParent{
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"]
me = "Maiku Suteda"
@@ -640,6 +617,7 @@ func TestUnmarshalTextMarshaler(t *testing.T) {
}
// TODO: Remove nolint once type and methods are used by a test
//
//nolint:unused
type precedentMarshaler struct {
FirstName string
@@ -658,6 +636,7 @@ func (m precedentMarshaler) MarshalTOML() ([]byte, error) {
}
// TODO: Remove nolint once type and method are used by a test
//
//nolint:unused
type customPointerMarshaler struct {
FirstName string
@@ -670,6 +649,7 @@ func (m *customPointerMarshaler) MarshalTOML() ([]byte, error) {
}
// TODO: Remove nolint once type and method are used by a test
//
//nolint:unused
type textPointerMarshaler struct {
FirstName string
@@ -682,6 +662,7 @@ func (m *textPointerMarshaler) MarshalText() ([]byte, error) {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var commentTestToml = []byte(`
# it's a comment on type
@@ -719,6 +700,7 @@ type mapsTestStruct struct {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var mapsTestData = mapsTestStruct{
Simple: map[string]string{
@@ -742,6 +724,7 @@ var mapsTestData = mapsTestStruct{
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var mapsTestToml = []byte(`
[Other]
@@ -764,6 +747,7 @@ var mapsTestToml = []byte(`
`)
// TODO: Remove nolint once type is used by a test
//
//nolint:deadcode,unused
type structArrayNoTag struct {
A struct {
@@ -773,6 +757,7 @@ type structArrayNoTag struct {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var customTagTestToml = []byte(`
[postgres]
@@ -787,6 +772,7 @@ var customTagTestToml = []byte(`
`)
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var customCommentTagTestToml = []byte(`
# db connection
@@ -800,6 +786,7 @@ var customCommentTagTestToml = []byte(`
`)
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var customCommentedTagTestToml = []byte(`
[postgres]
@@ -854,6 +841,7 @@ func TestUnmarshalTabInStringAndQuotedKey(t *testing.T) {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var customMultilineTagTestToml = []byte(`int_slice = [
1,
@@ -863,6 +851,7 @@ var customMultilineTagTestToml = []byte(`int_slice = [
`)
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var testDocBasicToml = []byte(`
[document]
@@ -875,12 +864,14 @@ var testDocBasicToml = []byte(`
`)
// TODO: Remove nolint once type is used by a test
//
//nolint:deadcode
type testDocCustomTag struct {
Doc testDocBasicsCustomTag `file:"document"`
}
// TODO: Remove nolint once type is used by a test
//
//nolint:deadcode
type testDocBasicsCustomTag struct {
Bool bool `file:"bool_val"`
@@ -893,6 +884,7 @@ type testDocBasicsCustomTag struct {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,varcheck
var testDocCustomTagData = testDocCustomTag{
Doc: testDocBasicsCustomTag{
@@ -956,6 +948,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) {
a := 1
err := toml.Unmarshal([]byte{}, a)
@@ -972,6 +987,7 @@ func TestUnmarshalInvalidPointerKind(t *testing.T) {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused
type testDuration struct {
Nanosec time.Duration `toml:"nanosec"`
@@ -986,6 +1002,7 @@ type testDuration struct {
}
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var testDurationToml = []byte(`
nanosec = "1ns"
@@ -1000,6 +1017,7 @@ a_string = "15s"
`)
// TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck
var testDurationToml2 = []byte(`a_string = "15s"
hour = "1h0m0s"
@@ -1013,6 +1031,7 @@ sec = "1s"
`)
// TODO: Remove nolint once type is used by a test
//
//nolint:deadcode,unused
type testBadDuration struct {
Val time.Duration `toml:"val"`
@@ -1066,10 +1085,6 @@ func TestUnmarshalCheckConversionFloatInt(t *testing.T) {
desc: "int",
input: `I = 1e300`,
},
{
desc: "float",
input: `F = 9223372036854775806`,
},
}
for _, test := range testCases {
@@ -1954,7 +1969,7 @@ func decoder(doc string) *toml.Decoder {
func strictDecoder(doc string) *toml.Decoder {
d := decoder(doc)
d.SetStrict(true)
d.DisallowUnknownFields()
return d
}
+23 -4
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"strconv"
"time"
"github.com/pelletier/go-toml/v2"
)
// Remove JSON tags to a data structure as returned by toml-test.
@@ -76,14 +78,31 @@ func untag(typed map[string]interface{}) (interface{}, error) {
return nil, fmt.Errorf("untag: %w", err)
}
return f, nil
//toml.LocalDate{Year:2020, Month:12, Day:12}
case "datetime":
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", false)
return time.Parse("2006-01-02T15:04:05.999999999Z07:00", v)
case "datetime-local":
return parseTime(v, "2006-01-02T15:04:05.999999999", true)
var t toml.LocalDateTime
err := t.UnmarshalText([]byte(v))
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return t, nil
case "date-local":
return parseTime(v, "2006-01-02", true)
var t toml.LocalDate
err := t.UnmarshalText([]byte(v))
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return t, nil
case "time-local":
return parseTime(v, "15:04:05.999999999", true)
var t toml.LocalTime
err := t.UnmarshalText([]byte(v))
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return t, nil
case "bool":
switch v {
case "true":
@@ -43,8 +43,26 @@ func DecodeStdin() error {
j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(addTag("", decoded)); err != nil {
fmt.Errorf("Error encoding JSON: %s", err)
return fmt.Errorf("Error encoding JSON: %s", err)
}
return nil
}
// EncodeStdin is a helper function for the toml-test binary interface. Tagged
// JSON is read from STDIN and a resulting TOML representation is written to
// STDOUT.
func EncodeStdin() error {
var j interface{}
err := json.NewDecoder(os.Stdin).Decode(&j)
if err != nil {
return err
}
rm, err := rmTag(j)
if err != nil {
return fmt.Errorf("removing tags: %w", err)
}
return toml.NewEncoder(os.Stdout).Encode(rm)
}
+5 -7
View File
@@ -1,8 +1,6 @@
package tracker
import (
"github.com/pelletier/go-toml/v2/internal/ast"
)
import "github.com/pelletier/go-toml/v2/unstable"
// KeyTracker is a tracker that keeps track of the current Key as the AST is
// walked.
@@ -11,19 +9,19 @@ type KeyTracker struct {
}
// UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node *ast.Node) {
func (t *KeyTracker) UpdateTable(node *unstable.Node) {
t.reset()
t.Push(node)
}
// UpdateArrayTable sets the state of the tracker with the AST array table node.
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
func (t *KeyTracker) UpdateArrayTable(node *unstable.Node) {
t.reset()
t.Push(node)
}
// Push the given key on the stack.
func (t *KeyTracker) Push(node *ast.Node) {
func (t *KeyTracker) Push(node *unstable.Node) {
it := node.Key()
for it.Next() {
t.k = append(t.k, string(it.Node().Data))
@@ -31,7 +29,7 @@ func (t *KeyTracker) Push(node *ast.Node) {
}
// Pop key from stack.
func (t *KeyTracker) Pop(node *ast.Node) {
func (t *KeyTracker) Pop(node *unstable.Node) {
it := node.Key()
for it.Next() {
t.k = t.k[:len(t.k)-1]
+181 -82
View File
@@ -3,8 +3,9 @@ package tracker
import (
"bytes"
"fmt"
"sync"
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/unstable"
)
type keyKind uint8
@@ -54,80 +55,130 @@ func (k keyKind) String() string {
type SeenTracker struct {
entries []entry
currentIdx int
nextID int
}
var pool = sync.Pool{
New: func() interface{} {
return &SeenTracker{}
},
}
func (s *SeenTracker) reset() {
// Always contains a root element at index 0.
s.currentIdx = 0
if len(s.entries) == 0 {
s.entries = make([]entry, 1, 2)
} else {
s.entries = s.entries[:1]
}
s.entries[0].child = -1
s.entries[0].next = -1
}
type entry struct {
id int
parent int
// Use -1 to indicate no child or no sibling.
child int
next int
name []byte
kind keyKind
explicit bool
kv bool
}
// Remove all descendent of node at position idx.
func (s *SeenTracker) clear(idx int) {
p := s.entries[idx].id
rest := clear(p, s.entries[idx+1:])
s.entries = s.entries[:idx+1+len(rest)]
}
func clear(parentID int, entries []entry) []entry {
for i := 0; i < len(entries); {
if entries[i].parent == parentID {
id := entries[i].id
copy(entries[i:], entries[i+1:])
entries = entries[:len(entries)-1]
rest := clear(id, entries[i:])
entries = entries[:i+len(rest)]
} else {
i++
// Find the index of the child of parentIdx with key k. Returns -1 if
// it does not exist.
func (s *SeenTracker) find(parentIdx int, k []byte) int {
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
if bytes.Equal(s.entries[i].name, k) {
return i
}
}
return entries
return -1
}
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool) int {
parentID := s.id(parentIdx)
// Remove all descendants of node at position idx.
func (s *SeenTracker) clear(idx int) {
if idx >= len(s.entries) {
return
}
for i := s.entries[idx].child; i >= 0; {
next := s.entries[i].next
n := s.entries[0].next
s.entries[0].next = i
s.entries[i].next = n
s.entries[i].name = nil
s.clear(i)
i = next
}
s.entries[idx].child = -1
}
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool, kv bool) int {
e := entry{
child: -1,
next: s.entries[parentIdx].child,
idx := len(s.entries)
s.entries = append(s.entries, entry{
id: s.nextID,
parent: parentID,
name: name,
kind: kind,
explicit: explicit,
})
s.nextID++
kv: kv,
}
var idx int
if s.entries[0].next >= 0 {
idx = s.entries[0].next
s.entries[0].next = s.entries[idx].next
s.entries[idx] = e
} else {
idx = len(s.entries)
s.entries = append(s.entries, e)
}
s.entries[parentIdx].child = idx
return idx
}
// CheckExpression takes a top-level node and checks that it does not contain keys
// that have been seen in previous calls, and validates that types are consistent.
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
func (s *SeenTracker) setExplicitFlag(parentIdx int) {
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
if s.entries[i].kv {
s.entries[i].explicit = true
s.entries[i].kv = false
}
s.setExplicitFlag(i)
}
}
// CheckExpression takes a top-level node and checks that it does not contain
// keys that have been seen in previous calls, and validates that types are
// consistent. It returns true if it is the first time this node's key is seen.
// Useful to clear array tables on first use.
func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) {
if s.entries == nil {
// Skip ID = 0 to remove the confusion between nodes whose parent has
// id 0 and root nodes (parent id is 0 because it's the zero value).
s.nextID = 1
// Start unscoped, so idx is negative.
s.currentIdx = -1
s.reset()
}
switch node.Kind {
case ast.KeyValue:
case unstable.KeyValue:
return s.checkKeyValue(node)
case ast.Table:
case unstable.Table:
return s.checkTable(node)
case ast.ArrayTable:
case unstable.ArrayTable:
return s.checkArrayTable(node)
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
}
}
func (s *SeenTracker) checkTable(node *ast.Node) error {
func (s *SeenTracker) checkTable(node *unstable.Node) (bool, error) {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
it := node.Key()
parentIdx := -1
parentIdx := 0
// This code is duplicated in checkArrayTable. This is because factoring
// it in a function requires to copy the iterator, or allocate it to the
@@ -142,7 +193,12 @@ func (s *SeenTracker) checkTable(node *ast.Node) error {
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false)
idx = s.create(parentIdx, k, tableKind, false, false)
} else {
entry := s.entries[idx]
if entry.kind == valueKind {
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
}
}
parentIdx = idx
}
@@ -150,28 +206,34 @@ func (s *SeenTracker) checkTable(node *ast.Node) error {
k := it.Node().Data
idx := s.find(parentIdx, k)
first := false
if idx >= 0 {
kind := s.entries[idx].kind
if kind != tableKind {
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
return false, fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
}
if s.entries[idx].explicit {
return fmt.Errorf("toml: table %s already exists", string(k))
return false, fmt.Errorf("toml: table %s already exists", string(k))
}
s.entries[idx].explicit = true
} else {
idx = s.create(parentIdx, k, tableKind, true)
idx = s.create(parentIdx, k, tableKind, true, false)
first = true
}
s.currentIdx = idx
return nil
return first, nil
}
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
it := node.Key()
parentIdx := -1
parentIdx := 0
for it.Next() {
if it.IsLast() {
@@ -183,77 +245,114 @@ func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false)
idx = s.create(parentIdx, k, tableKind, false, false)
} else {
entry := s.entries[idx]
if entry.kind == valueKind {
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
}
}
parentIdx = idx
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx >= 0 {
firstTime := idx < 0
if firstTime {
idx = s.create(parentIdx, k, arrayTableKind, true, false)
} else {
kind := s.entries[idx].kind
if kind != arrayTableKind {
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
}
s.clear(idx)
} else {
idx = s.create(parentIdx, k, arrayTableKind, true)
}
s.currentIdx = idx
return nil
return firstTime, nil
}
func (s *SeenTracker) checkKeyValue(node *ast.Node) error {
it := node.Key()
func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
parentIdx := s.currentIdx
it := node.Key()
for it.Next() {
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx >= 0 {
if s.entries[idx].kind != tableKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), s.entries[idx].kind)
}
if s.entries[idx].explicit {
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
}
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false, true)
} else {
idx = s.create(parentIdx, k, tableKind, false)
entry := s.entries[idx]
if it.IsLast() {
return false, fmt.Errorf("toml: key %s is already defined", string(k))
} else if entry.kind != tableKind {
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
} else if entry.explicit {
return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
}
}
parentIdx = idx
}
kind := valueKind
s.entries[parentIdx].kind = valueKind
if node.Value().Kind == ast.InlineTable {
kind = tableKind
value := node.Value()
switch value.Kind {
case unstable.InlineTable:
return s.checkInlineTable(value)
case unstable.Array:
return s.checkArray(value)
}
s.entries[parentIdx].kind = kind
return nil
return false, nil
}
func (s *SeenTracker) id(idx int) int {
if idx >= 0 {
return s.entries[idx].id
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
it := node.Children()
for it.Next() {
n := it.Node()
switch n.Kind {
case unstable.InlineTable:
first, err = s.checkInlineTable(n)
if err != nil {
return false, err
}
case unstable.Array:
first, err = s.checkArray(n)
if err != nil {
return false, err
}
}
}
return 0
return first, nil
}
func (s *SeenTracker) find(parentIdx int, k []byte) int {
parentID := s.id(parentIdx)
func (s *SeenTracker) checkInlineTable(node *unstable.Node) (first bool, err error) {
s = pool.Get().(*SeenTracker)
s.reset()
for i := parentIdx + 1; i < len(s.entries); i++ {
if s.entries[i].parent == parentID && bytes.Equal(s.entries[i].name, k) {
return i
it := node.Children()
for it.Next() {
n := it.Node()
first, err = s.checkKeyValue(n)
if err != nil {
return false, err
}
}
return -1
// As inline tables are self-contained, the tracker does not
// need to retain the details of what they contain. The
// keyValue element that creates the inline table is kept to
// mark the presence of the inline table and prevent
// redefinition of its keys: check* functions cannot walk into
// a value.
pool.Put(s)
return first, nil
}
+16
View File
@@ -0,0 +1,16 @@
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.
maxExpectedEntrySize := 48
require.LessOrEqual(t, int(unsafe.Sizeof(entry{})), maxExpectedEntrySize)
}
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"time"
"github.com/pelletier/go-toml/v2/unstable"
)
// LocalDate represents a calendar day in no specific timezone.
@@ -75,7 +77,7 @@ func (d LocalTime) MarshalText() ([]byte, error) {
func (d *LocalTime) UnmarshalText(b []byte) error {
res, left, err := parseLocalTime(b)
if err == nil && len(left) != 0 {
err = newDecodeError(left, "extra characters")
err = unstable.NewParserError(left, "extra characters")
}
if err != nil {
return err
@@ -109,7 +111,7 @@ func (d LocalDateTime) MarshalText() ([]byte, error) {
func (d *LocalDateTime) UnmarshalText(data []byte) error {
res, left, err := parseLocalDateTime(data)
if err == nil && len(left) != 0 {
err = newDecodeError(left, "extra characters")
err = unstable.NewParserError(left, "extra characters")
}
if err != nil {
return err
+420 -105
View File
@@ -3,6 +3,7 @@ package toml
import (
"bytes"
"encoding"
"encoding/json"
"fmt"
"io"
"math"
@@ -11,6 +12,9 @@ import (
"strconv"
"strings"
"time"
"unicode"
"github.com/pelletier/go-toml/v2/internal/characters"
)
// Marshal serializes a Go value as a TOML document.
@@ -34,10 +38,11 @@ type Encoder struct {
w io.Writer
// global settings
tablesInline bool
arraysMultiline bool
indentSymbol string
indentTables bool
tablesInline bool
arraysMultiline bool
indentSymbol string
indentTables bool
marshalJsonNumbers bool
}
// NewEncoder returns a new Encoder that writes to w.
@@ -53,9 +58,10 @@ func NewEncoder(w io.Writer) *Encoder {
// This behavior can be controlled on an individual struct field basis with the
// inline tag:
//
// MyField `inline:"true"`
func (enc *Encoder) SetTablesInline(inline bool) {
// MyField `toml:",inline"`
func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
enc.tablesInline = inline
return enc
}
// SetArraysMultiline forces the encoder to emit all arrays with one element per
@@ -63,28 +69,42 @@ func (enc *Encoder) SetTablesInline(inline bool) {
//
// This behavior can be controlled on an individual struct field basis with the multiline tag:
//
// MyField `multiline:"true"`
func (enc *Encoder) SetArraysMultiline(multiline bool) {
// MyField `multiline:"true"`
func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder {
enc.arraysMultiline = multiline
return enc
}
// SetIndentSymbol defines the string that should be used for indentation. The
// provided string is repeated for each indentation level. Defaults to two
// spaces.
func (enc *Encoder) SetIndentSymbol(s string) {
func (enc *Encoder) SetIndentSymbol(s string) *Encoder {
enc.indentSymbol = s
return enc
}
// SetIndentTables forces the encoder to intent tables and array tables.
func (enc *Encoder) SetIndentTables(indent bool) {
func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
enc.indentTables = indent
return enc
}
// SetMarshalJsonNumbers forces the encoder to serialize `json.Number` as a
// float or integer instead of relying on TextMarshaler to emit a string.
//
// *Unstable:* This method does not follow the compatibility guarantees of
// semver. It can be changed or removed without a new major version being
// issued.
func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder {
enc.marshalJsonNumbers = indent
return enc
}
// Encode writes a TOML representation of v to the stream.
//
// If v cannot be represented to TOML it returns an error.
//
// Encoding rules
// # Encoding rules
//
// A top level slice containing only maps or structs is encoded as [[table
// array]].
@@ -99,27 +119,55 @@ func (enc *Encoder) SetIndentTables(indent bool) {
// Intermediate tables are always printed.
//
// By default, strings are encoded as literal string, unless they contain either
// a newline character or a single quote. In that case they are emitted as quoted
// strings.
// a newline character or a single quote. In that case they are emitted as
// quoted strings.
//
// Unsigned integers larger than math.MaxInt64 cannot be encoded. Doing so
// results in an error. This rule exists because the TOML specification only
// requires parsers to support at least the 64 bits integer range. Allowing
// larger numbers would create non-standard TOML documents, which may not be
// readable (at best) by other implementations. To encode such numbers, a
// solution is a custom type that implements encoding.TextMarshaler.
//
// When encoding structs, fields are encoded in order of definition, with their
// exact name.
//
// Struct tags
// Tables and array tables are separated by empty lines. However, consecutive
// subtables definitions are not. For example:
//
// The following struct tags are available to tweak encoding on a per-field
// basis:
// [top1]
//
// toml:"foo"
// Changes the name of the key to use for the field to foo.
// [top2]
// [top2.child1]
//
// multiline:"true"
// When the field contains a string, it will be emitted as a quoted
// multi-line TOML string.
// [[array]]
//
// inline:"true"
// When the field would normally be encoded as a table, it is instead
// encoded as an inline table.
// [[array]]
// [array.child2]
//
// # Struct tags
//
// The encoding of each public struct field can be customized by the format
// string in the "toml" key of the struct field's tag. This follows
// encoding/json's convention. The format string starts with the name of the
// field, optionally followed by a comma-separated list of options. The name may
// be empty in order to provide options without overriding the default name.
//
// The "multiline" option emits strings as quoted multi-line TOML strings. It
// has no effect on fields that would not be encoded as strings.
//
// The "inline" option turns fields that would be emitted as tables into inline
// tables instead. It has no effect on other fields.
//
// The "omitempty" option prevents empty values or groups from being emitted.
//
// The "commented" option prefixes the value and all its children with a comment
// symbol.
//
// In addition to the "toml" tag struct tag, a "comment" tag can be used to emit
// a TOML comment before the value being annotated. Comments are ignored inside
// inline tables. For array tables, the comment is only present before the first
// element of the array.
func (enc *Encoder) Encode(v interface{}) error {
var (
b []byte
@@ -147,6 +195,9 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct {
multiline bool
omitempty bool
commented bool
comment string
}
type encoderCtx struct {
@@ -171,6 +222,9 @@ type encoderCtx struct {
// Indentation level
indent int
// Prefix the current value with a comment.
commented bool
// Options coming from struct tags
options valueOptions
}
@@ -196,16 +250,41 @@ func (ctx *encoderCtx) isRoot() bool {
return len(ctx.parentKey) == 0 && !ctx.hasKey
}
//nolint:cyclop,funlen
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if !v.IsZero() {
i, ok := v.Interface().(time.Time)
if ok {
return i.AppendFormat(b, time.RFC3339), nil
i := v.Interface()
switch x := i.(type) {
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
case json.Number:
if enc.marshalJsonNumbers {
if x == "" { /// Useful zero value.
return append(b, "0"...), nil
} else if v, err := x.Int64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(v))
} else if f, err := x.Float64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(f))
} else {
return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x)
}
}
}
if v.Type().Implements(textMarshalerType) {
hasTextMarshaler := v.Type().Implements(textMarshalerType)
if hasTextMarshaler || (v.CanAddr() && reflect.PtrTo(v.Type()).Implements(textMarshalerType)) {
if !hasTextMarshaler {
v = v.Addr()
}
if ctx.isRoot() {
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
}
@@ -226,7 +305,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
return enc.encodeMap(b, ctx, v)
case reflect.Struct:
return enc.encodeStruct(b, ctx, v)
case reflect.Slice:
case reflect.Slice, reflect.Array:
return enc.encodeSlice(b, ctx, v)
case reflect.Interface:
if v.IsNil() {
@@ -245,16 +324,31 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.String:
b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32:
if math.Trunc(v.Float()) == v.Float() {
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 32)
f := v.Float()
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 {
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
b = strconv.AppendFloat(b, f, 'f', -1, 32)
}
case reflect.Float64:
if math.Trunc(v.Float()) == v.Float() {
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 64)
f := v.Float()
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 {
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64)
b = strconv.AppendFloat(b, f, 'f', -1, 64)
}
case reflect.Bool:
if v.Bool() {
@@ -263,7 +357,11 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
b = append(b, "false"...)
}
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
b = strconv.AppendUint(b, v.Uint(), 10)
x := v.Uint()
if x > uint64(math.MaxInt64) {
return nil, fmt.Errorf("toml: not encoding uint (%d) greater than max int64 (%d)", x, int64(math.MaxInt64))
}
b = strconv.AppendUint(b, x, 10)
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10)
default:
@@ -282,19 +380,20 @@ func isNil(v reflect.Value) bool {
}
}
func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
return options.omitempty && isEmptyValue(v)
}
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error
if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context")
}
b = enc.indent(ctx.indent, b)
b, err = enc.encodeKey(b, ctx.key)
if err != nil {
return nil, err
if !ctx.inline {
b = enc.encodeComment(ctx.indent, options.comment, b)
b = enc.commented(ctx.commented, b)
b = enc.indent(ctx.indent, b)
}
b = enc.encodeKey(b, ctx.key)
b = append(b, " = "...)
// create a copy of the context because the value of a KV shouldn't
@@ -312,6 +411,61 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
return b, nil
}
func (enc *Encoder) commented(commented bool, b []byte) []byte {
if commented {
return append(b, "# "...)
}
return b
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Struct:
return isEmptyStruct(v)
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func isEmptyStruct(v reflect.Value) bool {
// TODO: merge with walkStruct and cache.
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
fieldType := typ.Field(i)
// only consider exported fields
if fieldType.PkgPath != "" {
continue
}
tag := fieldType.Tag.Get("toml")
// special field name to skip field
if tag == "-" {
continue
}
f := v.Field(i)
if !isEmptyValue(f) {
return false
}
}
return true
}
const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
@@ -323,7 +477,13 @@ func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byt
}
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' || characters.InvalidAscii(b) {
return true
}
}
return false
}
// caller should have checked that the string does not contain new lines or ' .
@@ -335,7 +495,6 @@ func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
return b
}
//nolint:cyclop
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
stringQuote := `"`
@@ -395,7 +554,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
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 {
return append(b, v...)
}
@@ -405,24 +564,19 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
return b, nil
}
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
b = enc.commented(ctx.commented, b)
b = enc.indent(ctx.indent, b)
b = append(b, '[')
var err error
b, err = enc.encodeKey(b, ctx.parentKey[0])
if err != nil {
return nil, err
}
b = enc.encodeKey(b, ctx.parentKey[0])
for _, k := range ctx.parentKey[1:] {
b = append(b, '.')
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
}
b = enc.encodeKey(b, k)
}
b = append(b, "]\n"...)
@@ -431,19 +585,19 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
}
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
func (enc *Encoder) encodeKey(b []byte, k string) []byte {
needsQuotation := false
cannotUseLiteral := false
if len(k) == 0 {
return append(b, "''"...)
}
for _, c := range k {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue
}
if c == '\n' {
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
}
if c == literalQuote {
cannotUseLiteral = true
}
@@ -451,21 +605,37 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation = true
}
if needsQuotation && needsQuoting(k) {
cannotUseLiteral = true
}
switch {
case cannotUseLiteral:
return enc.encodeQuotedString(false, b, k), nil
return enc.encodeQuotedString(false, b, k)
case needsQuotation:
return enc.encodeLiteralString(b, k), nil
return enc.encodeLiteralString(b, k)
default:
return enc.encodeUnquotedKey(b, k), nil
return enc.encodeUnquotedKey(b, k)
}
}
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("toml: type %s is not supported as a map key", v.Type().Key().Kind())
}
func (enc *Encoder) keyToString(k reflect.Value) (string, error) {
keyType := k.Type()
switch {
case keyType.Kind() == reflect.String:
return k.String(), nil
case keyType.Implements(textMarshalerType):
keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil {
return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err)
}
return string(keyB), nil
}
return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind())
}
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var (
t table
emptyValueOptions valueOptions
@@ -473,13 +643,17 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
iter := v.MapRange()
for iter.Next() {
k := iter.Key().String()
v := iter.Value()
if isNil(v) {
continue
}
k, err := enc.keyToString(iter.Key())
if err != nil {
return nil, err
}
if willConvertToTableOrArrayTable(ctx, v) {
t.pushTable(k, v, emptyValueOptions)
} else {
@@ -511,18 +685,26 @@ type table struct {
}
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})
}
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})
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table
//nolint:godox
// TODO: cache this?
func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
// TODO: cache this
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
fieldType := typ.Field(i)
@@ -532,45 +714,136 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue
}
k, ok := fieldType.Tag.Lookup("toml")
if !ok {
k = fieldType.Name
}
tag := fieldType.Tag.Get("toml")
// special field name to skip field
if k == "-" {
if tag == "-" {
continue
}
k, opts := parseTag(tag)
if !isValidName(k) {
k = ""
}
f := v.Field(i)
if k == "" {
if fieldType.Anonymous {
if fieldType.Type.Kind() == reflect.Struct {
walkStruct(ctx, t, f)
} else if fieldType.Type.Kind() == reflect.Pointer && !f.IsNil() && f.Elem().Kind() == reflect.Struct {
walkStruct(ctx, t, f.Elem())
}
continue
} else {
k = fieldType.Name
}
}
if isNil(f) {
continue
}
options := valueOptions{
multiline: fieldBoolTag(fieldType, "multiline"),
multiline: opts.multiline,
omitempty: opts.omitempty,
commented: opts.commented,
comment: fieldType.Tag.Get("comment"),
}
inline := fieldBoolTag(fieldType, "inline")
if inline || !willConvertToTableOrArrayTable(ctx, f) {
if opts.inline || !willConvertToTableOrArrayTable(ctx, f) {
t.pushKV(k, f, options)
} else {
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)
}
func fieldBoolTag(field reflect.StructField, tag string) bool {
x, ok := field.Tag.Lookup(tag)
return ok && x == "true"
func (enc *Encoder) encodeComment(indent int, comment string, b []byte) []byte {
for len(comment) > 0 {
var line string
idx := strings.IndexByte(comment, '\n')
if idx >= 0 {
line = comment[:idx]
comment = comment[idx+1:]
} else {
line = comment
comment = ""
}
b = enc.indent(indent, b)
b = append(b, "# "...)
b = append(b, line...)
b = append(b, '\n')
}
return b
}
func isValidName(s string) bool {
if s == "" {
return false
}
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
case !unicode.IsLetter(c) && !unicode.IsDigit(c):
return false
}
}
return true
}
type tagOptions struct {
multiline bool
inline bool
omitempty bool
commented bool
}
func parseTag(tag string) (string, tagOptions) {
opts := tagOptions{}
idx := strings.Index(tag, ",")
if idx == -1 {
return tag, opts
}
raw := tag[idx+1:]
tag = string(tag[:idx])
for raw != "" {
var o string
i := strings.Index(raw, ",")
if i >= 0 {
o, raw = raw[:i], raw[i+1:]
} else {
o, raw = raw, ""
}
switch o {
case "multiline":
opts.multiline = true
case "inline":
opts.inline = true
case "omitempty":
opts.omitempty = true
case "commented":
opts.commented = true
}
}
return tag, opts
}
//nolint:cyclop
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error
@@ -592,10 +865,18 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
}
ctx.skipTableHeader = false
hasNonEmptyKV := false
for _, kv := range t.kvs {
ctx.setKey(kv.Key)
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
ctx.setKey(kv.Key)
ctx2 := ctx
ctx2.commented = kv.Options.commented || ctx2.commented
b, err = enc.encodeKv(b, ctx2, kv.Options, kv.Value)
if err != nil {
return nil, err
}
@@ -603,17 +884,30 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
b = append(b, '\n')
}
first := true
for _, table := range t.tables {
if shouldOmitEmpty(table.Options, table.Value) {
continue
}
if first {
first = false
if hasNonEmptyKV {
b = append(b, '\n')
}
} else {
b = append(b, "\n"...)
}
ctx.setKey(table.Key)
ctx.options = table.Options
ctx2 := ctx
ctx2.commented = ctx2.commented || ctx.options.commented
b, err = enc.encode(b, ctx, table.Value)
b, err = enc.encode(b, ctx2, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
return b, nil
@@ -626,6 +920,10 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
first := true
for _, kv := range t.kvs {
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if first {
first = false
} else {
@@ -641,7 +939,7 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
}
if len(t.tables) > 0 {
panic("inline table cannot contain nested tables, online key-values")
panic("inline table cannot contain nested tables, only key-values")
}
b = append(b, "}"...)
@@ -653,7 +951,7 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
if !v.IsValid() {
return false
}
if v.Type() == timeType || v.Type().Implements(textMarshalerType) {
if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PtrTo(v.Type()).Implements(textMarshalerType)) {
return false
}
@@ -675,13 +973,16 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
}
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
if ctx.insideKv {
return false
}
t := v.Type()
if t.Kind() == reflect.Interface {
return willConvertToTableOrArrayTable(ctx, v.Elem())
}
if t.Kind() == reflect.Slice {
if t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
if v.Len() == 0 {
// An empty slice should be a kv = [].
return false
@@ -720,8 +1021,14 @@ 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) {
ctx.shiftKey()
var err error
scratch := make([]byte, 0, 64)
scratch = enc.commented(ctx.commented, scratch)
if enc.indentTables {
scratch = enc.indent(ctx.indent, scratch)
}
scratch = append(scratch, "[["...)
for i, k := range ctx.parentKey {
@@ -729,18 +1036,26 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
scratch = append(scratch, '.')
}
scratch, err = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
scratch = enc.encodeKey(scratch, k)
}
scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
if enc.indentTables {
ctx.indent++
}
for i := 0; i < v.Len(); i++ {
if i != 0 {
b = append(b, "\n"...)
}
b = append(b, scratch...)
var err error
b, err = enc.encode(b, ctx, v.Index(i))
if err != nil {
return nil, err
+973 -100
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
//go:build go1.18 || go1.19 || go1.20 || go1.21 || go1.22
// +build go1.18 go1.19 go1.20 go1.21 go1.22
package ossfuzz
import (
"fmt"
"reflect"
"strings"
"github.com/pelletier/go-toml/v2"
)
func FuzzToml(data []byte) int {
if len(data) >= 2048 {
return 0
}
if strings.Contains(string(data), "nan") {
return 0
}
var v interface{}
err := toml.Unmarshal(data, &v)
if err != nil {
return 0
}
encoded, err := toml.Marshal(v)
if err != nil {
panic(fmt.Sprintf("failed to marshal unmarshaled document: %s", err))
}
var v2 interface{}
err = toml.Unmarshal(encoded, &v2)
if err != nil {
panic(fmt.Sprintf("failed round trip: %s", err))
}
if !reflect.DeepEqual(v, v2) {
panic(fmt.Sprintf("not equal: %#+v %#+v", v, v2))
}
return 1
}
-431
View File
@@ -1,431 +0,0 @@
package toml
import (
"testing"
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/stretchr/testify/require"
)
//nolint:funlen
func TestParser_AST_Numbers(t *testing.T) {
examples := []struct {
desc string
input string
kind ast.Kind
err bool
}{
{
desc: "integer just digits",
input: `1234`,
kind: ast.Integer,
},
{
desc: "integer zero",
input: `0`,
kind: ast.Integer,
},
{
desc: "integer sign",
input: `+99`,
kind: ast.Integer,
},
{
desc: "integer hex uppercase",
input: `0xDEADBEEF`,
kind: ast.Integer,
},
{
desc: "integer hex lowercase",
input: `0xdead_beef`,
kind: ast.Integer,
},
{
desc: "integer octal",
input: `0o01234567`,
kind: ast.Integer,
},
{
desc: "integer binary",
input: `0b11010110`,
kind: ast.Integer,
},
{
desc: "float zero",
input: `0.0`,
kind: ast.Float,
},
{
desc: "float positive zero",
input: `+0.0`,
kind: ast.Float,
},
{
desc: "float negative zero",
input: `-0.0`,
kind: ast.Float,
},
{
desc: "float pi",
input: `3.1415`,
kind: ast.Float,
},
{
desc: "float negative",
input: `-0.01`,
kind: ast.Float,
},
{
desc: "float signed exponent",
input: `5e+22`,
kind: ast.Float,
},
{
desc: "float exponent lowercase",
input: `1e06`,
kind: ast.Float,
},
{
desc: "float exponent uppercase",
input: `-2E-2`,
kind: ast.Float,
},
{
desc: "float fractional with exponent",
input: `6.626e-34`,
kind: ast.Float,
},
{
desc: "float underscores",
input: `224_617.445_991_228`,
kind: ast.Float,
},
{
desc: "inf",
input: `inf`,
kind: ast.Float,
},
{
desc: "inf negative",
input: `-inf`,
kind: ast.Float,
},
{
desc: "inf positive",
input: `+inf`,
kind: ast.Float,
},
{
desc: "nan",
input: `nan`,
kind: ast.Float,
},
{
desc: "nan negative",
input: `-nan`,
kind: ast.Float,
},
{
desc: "nan positive",
input: `+nan`,
kind: ast.Float,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := parser{}
p.Reset([]byte(`A = ` + e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
expected := astNode{
Kind: ast.KeyValue,
Children: []astNode{
{Kind: e.kind, Data: []byte(e.input)},
{Kind: ast.Key, Data: []byte(`A`)},
},
}
compareNode(t, expected, p.Expression())
}
})
}
}
type (
astNode struct {
Kind ast.Kind
Data []byte
Children []astNode
}
)
func compareNode(t *testing.T, e astNode, n *ast.Node) {
t.Helper()
require.Equal(t, e.Kind, n.Kind)
require.Equal(t, e.Data, n.Data)
compareIterator(t, e.Children, n.Children())
}
func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
t.Helper()
idx := 0
for actual.Next() {
n := actual.Node()
if idx >= len(expected) {
t.Fatal("extra child in actual tree")
}
e := expected[idx]
compareNode(t, e, n)
idx++
}
if idx < len(expected) {
t.Fatal("missing children in actual", "idx =", idx, "expected =", len(expected))
}
}
//nolint:funlen
func TestParser_AST(t *testing.T) {
examples := []struct {
desc string
input string
ast astNode
err bool
}{
{
desc: "simple string assignment",
input: `A = "hello"`,
ast: astNode{
Kind: ast.KeyValue,
Children: []astNode{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "simple bool assignment",
input: `A = true`,
ast: astNode{
Kind: ast.KeyValue,
Children: []astNode{
{
Kind: ast.Bool,
Data: []byte(`true`),
},
{
Kind: ast.Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "array of strings",
input: `A = ["hello", ["world", "again"]]`,
ast: astNode{
Kind: ast.KeyValue,
Children: []astNode{
{
Kind: ast.Array,
Children: []astNode{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.Array,
Children: []astNode{
{
Kind: ast.String,
Data: []byte(`world`),
},
{
Kind: ast.String,
Data: []byte(`again`),
},
},
},
},
},
{
Kind: ast.Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "array of arrays of strings",
input: `A = ["hello", "world"]`,
ast: astNode{
Kind: ast.KeyValue,
Children: []astNode{
{
Kind: ast.Array,
Children: []astNode{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.String,
Data: []byte(`world`),
},
},
},
{
Kind: ast.Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "inline table",
input: `name = { first = "Tom", last = "Preston-Werner" }`,
ast: astNode{
Kind: ast.KeyValue,
Children: []astNode{
{
Kind: ast.InlineTable,
Children: []astNode{
{
Kind: ast.KeyValue,
Children: []astNode{
{Kind: ast.String, Data: []byte(`Tom`)},
{Kind: ast.Key, Data: []byte(`first`)},
},
},
{
Kind: ast.KeyValue,
Children: []astNode{
{Kind: ast.String, Data: []byte(`Preston-Werner`)},
{Kind: ast.Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: ast.Key,
Data: []byte(`name`),
},
},
},
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := parser{}
p.Reset([]byte(e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
compareNode(t, e.ast, p.Expression())
}
})
}
}
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
p := &parser{}
b.Run("4", func(b *testing.B) {
input := []byte(`"\u1234\u5678\u9ABC\u1234\u5678\u9ABC"`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for i := 0; i < b.N; i++ {
p.parseBasicString(input)
}
})
b.Run("8", func(b *testing.B) {
input := []byte(`"\u12345678\u9ABCDEF0\u12345678\u9ABCDEF0"`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for i := 0; i < b.N; i++ {
p.parseBasicString(input)
}
})
}
func TestParser_AST_DateTimes(t *testing.T) {
examples := []struct {
desc string
input string
kind ast.Kind
err bool
}{
{
desc: "offset-date-time with delim 'T' and UTC offset",
input: `2021-07-21T12:08:05Z`,
kind: ast.DateTime,
},
{
desc: "offset-date-time with space delim and +8hours offset",
input: `2021-07-21 12:08:05+08:00`,
kind: ast.DateTime,
},
{
desc: "local-date-time with nano second",
input: `2021-07-21T12:08:05.666666666`,
kind: ast.LocalDateTime,
},
{
desc: "local-date-time",
input: `2021-07-21T12:08:05`,
kind: ast.LocalDateTime,
},
{
desc: "local-date",
input: `2021-07-21`,
kind: ast.LocalDate,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := parser{}
p.Reset([]byte(`A = ` + e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
expected := astNode{
Kind: ast.KeyValue,
Children: []astNode{
{Kind: e.kind, Data: []byte(e.input)},
{Kind: ast.Key, Data: []byte(`A`)},
},
}
compareNode(t, expected, p.Expression())
}
})
}
}
+17 -17
View File
@@ -1,9 +1,9 @@
package toml
import (
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable"
)
type strict struct {
@@ -12,10 +12,10 @@ type strict struct {
// Tracks the current key being processed.
key tracker.KeyTracker
missing []decodeError
missing []unstable.ParserError
}
func (s *strict) EnterTable(node *ast.Node) {
func (s *strict) EnterTable(node *unstable.Node) {
if !s.Enabled {
return
}
@@ -23,7 +23,7 @@ func (s *strict) EnterTable(node *ast.Node) {
s.key.UpdateTable(node)
}
func (s *strict) EnterArrayTable(node *ast.Node) {
func (s *strict) EnterArrayTable(node *unstable.Node) {
if !s.Enabled {
return
}
@@ -31,7 +31,7 @@ func (s *strict) EnterArrayTable(node *ast.Node) {
s.key.UpdateArrayTable(node)
}
func (s *strict) EnterKeyValue(node *ast.Node) {
func (s *strict) EnterKeyValue(node *unstable.Node) {
if !s.Enabled {
return
}
@@ -39,7 +39,7 @@ func (s *strict) EnterKeyValue(node *ast.Node) {
s.key.Push(node)
}
func (s *strict) ExitKeyValue(node *ast.Node) {
func (s *strict) ExitKeyValue(node *unstable.Node) {
if !s.Enabled {
return
}
@@ -47,27 +47,27 @@ func (s *strict) ExitKeyValue(node *ast.Node) {
s.key.Pop(node)
}
func (s *strict) MissingTable(node *ast.Node) {
func (s *strict) MissingTable(node *unstable.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
message: "missing table",
key: s.key.Key(),
s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node),
Message: "missing table",
Key: s.key.Key(),
})
}
func (s *strict) MissingField(node *ast.Node) {
func (s *strict) MissingField(node *unstable.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
message: "missing field",
key: s.key.Key(),
s.missing = append(s.missing, unstable.ParserError{
Highlight: keyLocation(node),
Message: "missing field",
Key: s.key.Key(),
})
}
@@ -88,7 +88,7 @@ func (s *strict) Error(doc []byte) error {
return err
}
func keyLocation(node *ast.Node) []byte {
func keyLocation(node *unstable.Node) []byte {
k := node.Key()
hasOne := k.Next()
@@ -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")
+2 -1
View File
@@ -1,3 +1,4 @@
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@master -copy ./tests
//go:generate go run ./cmd/tomltestgen/main.go -o toml_testgen_test.go
// This is a support file for toml_testgen_test.go
@@ -8,7 +9,7 @@ import (
"testing"
"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"
)
+1516 -310
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -6,8 +6,9 @@ import (
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
var sliceInterfaceType = reflect.TypeOf([]interface{}{})
var timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
var sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
var stringType = reflect.TypeOf("")
+361 -193
View File
File diff suppressed because it is too large Load Diff
+1442 -340
View File
File diff suppressed because it is too large Load Diff
+136
View File
@@ -0,0 +1,136 @@
package unstable
import (
"fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// Iterator over a sequence of nodes.
//
// Starts uninitialized, you need to call Next() first.
//
// For example:
//
// it := n.Children()
// for it.Next() {
// n := it.Node()
// // do something with n
// }
type Iterator struct {
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a
// node, false otherwise.
func (c *Iterator) Next() bool {
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
}
return c.node.Valid()
}
// IsLast returns true if the current node of the iterator is the last
// one. Subsequent calls to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next == 0
}
// Node returns a pointer to the node pointed at by the iterator.
func (c *Iterator) Node() *Node {
return c.node
}
// Node in a TOML expression AST.
//
// Depending on Kind, its sequence of children should be interpreted
// differently.
//
// - Array have one child per element in the array.
// - InlineTable have one child per key-value in the table (each of kind
// InlineTable).
// - KeyValue have at least two children. The first one is the value. The rest
// make a potentially dotted key.
// - Table and ArrayTable's children represent a dotted key (same as
// KeyValue, but without the first node being the value).
//
// When relevant, Raw describes the range of bytes this node is referring to in
// the input document. Use Parser.Raw() to retrieve the actual bytes.
type Node struct {
Kind Kind
Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input).
// References to other nodes, as offsets in the backing array
// from this node. References can go backward, so those can be
// negative.
next int // 0 if last element
child int // 0 if no child
}
// Range of bytes in the document.
type Range struct {
Offset uint32
Length uint32
}
// Next returns a pointer to the next node, or nil if there is no next node.
func (n *Node) Next() *Node {
if n.next == 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.next))
}
// Child returns a pointer to the first child node of this node. Other children
// can be accessed calling Next on the first child. Returns an nil if this Node
// has no child.
func (n *Node) Child() *Node {
if n.child == 0 {
return nil
}
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.child))
}
// Valid returns true if the node's kind is set (not to Invalid).
func (n *Node) Valid() bool {
return n != nil
}
// Key returns the children nodes making the Key on a supported node. Panics
// otherwise. They are guaranteed to be all be of the Kind Key. A simple key
// would return just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(fmt.Errorf("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil. Panics if not called on a KeyValue node,
// or if the Children are malformed.
func (n *Node) Value() *Node {
return n.Child()
}
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
}
@@ -1,4 +1,4 @@
package toml
package unstable
import (
"bytes"
@@ -55,7 +55,7 @@ func BenchmarkParseLiteralStringValid(b *testing.B) {
for name, input := range inputs {
b.Run(name, func(b *testing.B) {
p := parser{}
p := Parser{}
b.SetBytes(int64(len(input)))
b.ReportAllocs()
b.ResetTimer()
+71
View File
@@ -0,0 +1,71 @@
package unstable
// root contains a full AST.
//
// It is immutable once constructed with Builder.
type root struct {
nodes []Node
}
// Iterator over the top level nodes.
func (r *root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = &r.nodes[0]
}
return it
}
func (r *root) at(idx reference) *Node {
return &r.nodes[idx]
}
type reference int
const invalidReference reference = -1
func (r reference) Valid() bool {
return r != invalidReference
}
type builder struct {
tree root
lastIdx int
}
func (b *builder) Tree() *root {
return &b.tree
}
func (b *builder) NodeAt(ref reference) *Node {
return b.tree.at(ref)
}
func (b *builder) Reset() {
b.tree.nodes = b.tree.nodes[:0]
b.lastIdx = 0
}
func (b *builder) Push(n Node) reference {
b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
return reference(b.lastIdx)
}
func (b *builder) PushAndChain(n Node) reference {
newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
}
b.lastIdx = newIdx
return reference(b.lastIdx)
}
func (b *builder) AttachChild(parent reference, child reference) {
b.tree.nodes[parent].child = int(child) - int(parent)
}
func (b *builder) Chain(from reference, to reference) {
b.tree.nodes[from].next = int(to) - int(from)
}
+3
View File
@@ -0,0 +1,3 @@
// Package unstable provides APIs that do not meet the backward compatibility
// guarantees yet.
package unstable
+7 -5
View File
@@ -1,25 +1,26 @@
package ast
package unstable
import "fmt"
// Kind represents the type of TOML structure contained in a given Node.
type Kind int
const (
// meta
// Meta
Invalid Kind = iota
Comment
Key
// top level structures
// Top level structures
Table
ArrayTable
KeyValue
// containers values
// Containers values
Array
InlineTable
// values
// Values
String
Bool
Float
@@ -30,6 +31,7 @@ const (
DateTime
)
// String implementation of fmt.Stringer.
func (k Kind) String() string {
switch k {
case Invalid:
+306 -150
View File
@@ -1,50 +1,108 @@
package toml
package unstable
import (
"bytes"
"fmt"
"unicode"
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/characters"
"github.com/pelletier/go-toml/v2/internal/danger"
)
type parser struct {
builder ast.Builder
ref ast.Reference
// ParserError describes an error relative to the content of the document.
//
// It cannot outlive the instance of Parser it refers to, and may cause panics
// if the parser is reset.
type ParserError struct {
Highlight []byte
Message string
Key []string // optional
}
// Error is the implementation of the error interface.
func (e *ParserError) Error() string {
return e.Message
}
// NewParserError is a convenience function to create a ParserError
//
// Warning: Highlight needs to be a subslice of Parser.data, so only slices
// returned by Parser.Raw are valid candidates.
func NewParserError(highlight []byte, format string, args ...interface{}) error {
return &ParserError{
Highlight: highlight,
Message: fmt.Errorf(format, args...).Error(),
}
}
// Parser scans over a TOML-encoded document and generates an iterative AST.
//
// To prime the Parser, first reset it with the contents of a TOML document.
// Then, process all top-level expressions sequentially. See Example.
//
// Don't forget to check Error() after you're done parsing.
//
// Each top-level expression needs to be fully processed before calling
// NextExpression() again. Otherwise, calls to various Node methods may panic if
// the parser has moved on the next expression.
//
// For performance reasons, go-toml doesn't make a copy of the input bytes to
// the parser. Make sure to copy all the bytes you need to outlive the slice
// given to the parser.
type Parser struct {
data []byte
builder builder
ref reference
left []byte
err error
first bool
KeepComments bool
}
func (p *parser) Range(b []byte) ast.Range {
return ast.Range{
// Data returns the slice provided to the last call to Reset.
func (p *Parser) Data() []byte {
return p.data
}
// Range returns a range description that corresponds to a given slice of the
// input. If the argument is not a subslice of the parser input, this function
// panics.
func (p *Parser) Range(b []byte) Range {
return Range{
Offset: uint32(danger.SubsliceOffset(p.data, b)),
Length: uint32(len(b)),
}
}
func (p *parser) Raw(raw ast.Range) []byte {
// Raw returns the slice corresponding to the bytes in the given range.
func (p *Parser) Raw(raw Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length]
}
func (p *parser) Reset(b []byte) {
// Reset brings the parser to its initial state for a given input. It wipes an
// reuses internal storage to reduce allocation.
func (p *Parser) Reset(b []byte) {
p.builder.Reset()
p.ref = ast.InvalidReference
p.ref = invalidReference
p.data = b
p.left = b
p.err = nil
p.first = true
}
//nolint:cyclop
func (p *parser) NextExpression() bool {
// NextExpression parses the next top-level expression. If an expression was
// successfully parsed, it returns true. If the parser is at the end of the
// document or an error occurred, it returns false.
//
// Retrieve the parsed expression with Expression().
func (p *Parser) NextExpression() bool {
if len(p.left) == 0 || p.err != nil {
return false
}
p.builder.Reset()
p.ref = ast.InvalidReference
p.ref = invalidReference
for {
if len(p.left) == 0 || p.err != nil {
@@ -73,15 +131,56 @@ func (p *parser) NextExpression() bool {
}
}
func (p *parser) Expression() *ast.Node {
// Expression returns a pointer to the node representing the last successfully
// parsed expression.
func (p *Parser) Expression() *Node {
return p.builder.NodeAt(p.ref)
}
func (p *parser) Error() error {
// Error returns any error that has occurred during parsing.
func (p *Parser) Error() error {
return p.err
}
func (p *parser) parseNewline(b []byte) ([]byte, error) {
// Position describes a position in the input.
type Position struct {
// Number of bytes from the beginning of the input.
Offset int
// Line number, starting at 1.
Line int
// Column number, starting at 1.
Column int
}
// Shape describes the position of a range in the input.
type Shape struct {
Start Position
End Position
}
func (p *Parser) position(b []byte) Position {
offset := danger.SubsliceOffset(p.data, b)
lead := p.data[:offset]
return Position{
Offset: offset,
Line: bytes.Count(lead, []byte{'\n'}) + 1,
Column: len(lead) - bytes.LastIndex(lead, []byte{'\n'}),
}
}
// Shape returns the shape of the given range in the input. Will
// panic if the range is not a subslice of the input.
func (p *Parser) Shape(r Range) Shape {
raw := p.Raw(r)
return Shape{
Start: p.position(raw),
End: p.position(raw[r.Length:]),
}
}
func (p *Parser) parseNewline(b []byte) ([]byte, error) {
if b[0] == '\n' {
return b[1:], nil
}
@@ -91,14 +190,27 @@ func (p *parser) parseNewline(b []byte) ([]byte, error) {
return rest, err
}
return nil, newDecodeError(b[0:1], "expected newline but got %#U", b[0])
return nil, NewParserError(b[0:1], "expected newline but got %#U", b[0])
}
func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseComment(b []byte) (reference, []byte, error) {
ref := invalidReference
data, rest, err := scanComment(b)
if p.KeepComments && err == nil {
ref = p.builder.Push(Node{
Kind: Comment,
Raw: p.Range(data),
Data: data,
})
}
return ref, rest, err
}
func (p *Parser) parseExpression(b []byte) (reference, []byte, error) {
// expression = ws [ comment ]
// expression =/ ws keyval ws [ comment ]
// expression =/ ws table ws [ comment ]
ref := ast.InvalidReference
ref := invalidReference
b = p.parseWhitespace(b)
@@ -107,7 +219,7 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
}
if b[0] == '#' {
_, rest, err := scanComment(b)
ref, rest, err := p.parseComment(b)
return ref, rest, err
}
@@ -129,14 +241,17 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) > 0 && b[0] == '#' {
_, rest, err := scanComment(b)
cref, rest, err := p.parseComment(b)
if cref != invalidReference {
p.builder.Chain(ref, cref)
}
return ref, rest, err
}
return ref, b, nil
}
func (p *parser) parseTable(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseTable(b []byte) (reference, []byte, error) {
// table = std-table / array-table
if len(b) > 1 && b[1] == '[' {
return p.parseArrayTable(b)
@@ -145,12 +260,12 @@ func (p *parser) parseTable(b []byte) (ast.Reference, []byte, error) {
return p.parseStdTable(b)
}
func (p *parser) parseArrayTable(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseArrayTable(b []byte) (reference, []byte, error) {
// array-table = array-table-open key array-table-close
// array-table-open = %x5B.5B ws ; [[ Double left square bracket
// array-table-close = ws %x5D.5D ; ]] Double right square bracket
ref := p.builder.Push(ast.Node{
Kind: ast.ArrayTable,
ref := p.builder.Push(Node{
Kind: ArrayTable,
})
b = b[2:]
@@ -174,12 +289,12 @@ func (p *parser) parseArrayTable(b []byte) (ast.Reference, []byte, error) {
return ref, b, err
}
func (p *parser) parseStdTable(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseStdTable(b []byte) (reference, []byte, error) {
// std-table = std-table-open key std-table-close
// std-table-open = %x5B ws ; [ Left square bracket
// std-table-close = ws %x5D ; ] Right square bracket
ref := p.builder.Push(ast.Node{
Kind: ast.Table,
ref := p.builder.Push(Node{
Kind: Table,
})
b = b[1:]
@@ -199,15 +314,15 @@ func (p *parser) parseStdTable(b []byte) (ast.Reference, []byte, error) {
return ref, b, err
}
func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) {
// keyval = key keyval-sep val
ref := p.builder.Push(ast.Node{
Kind: ast.KeyValue,
ref := p.builder.Push(Node{
Kind: KeyValue,
})
key, b, err := p.parseKey(b)
if err != nil {
return ast.InvalidReference, nil, err
return invalidReference, nil, err
}
// keyval-sep = ws %x3D ws ; =
@@ -215,12 +330,12 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return ast.InvalidReference, nil, newDecodeError(b, "expected = after a key, but the document ends there")
return invalidReference, nil, NewParserError(b, "expected = after a key, but the document ends there")
}
b, err = expect('=', b)
if err != nil {
return ast.InvalidReference, nil, err
return invalidReference, nil, err
}
b = p.parseWhitespace(b)
@@ -237,12 +352,12 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
}
//nolint:cyclop,funlen
func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseVal(b []byte) (reference, []byte, error) {
// val = string / boolean / array / inline-table / date-time / float / integer
ref := ast.InvalidReference
ref := invalidReference
if len(b) == 0 {
return ref, nil, newDecodeError(b, "expected value, not eof")
return ref, nil, NewParserError(b, "expected value, not eof")
}
var err error
@@ -259,8 +374,8 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
}
if err == nil {
ref = p.builder.Push(ast.Node{
Kind: ast.String,
ref = p.builder.Push(Node{
Kind: String,
Raw: p.Range(raw),
Data: v,
})
@@ -277,8 +392,8 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
}
if err == nil {
ref = p.builder.Push(ast.Node{
Kind: ast.String,
ref = p.builder.Push(Node{
Kind: String,
Raw: p.Range(raw),
Data: v,
})
@@ -287,22 +402,22 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
return ref, b, err
case 't':
if !scanFollowsTrue(b) {
return ref, nil, newDecodeError(atmost(b, 4), "expected 'true'")
return ref, nil, NewParserError(atmost(b, 4), "expected 'true'")
}
ref = p.builder.Push(ast.Node{
Kind: ast.Bool,
ref = p.builder.Push(Node{
Kind: Bool,
Data: b[:4],
})
return ref, b[4:], nil
case 'f':
if !scanFollowsFalse(b) {
return ref, nil, newDecodeError(atmost(b, 5), "expected 'false'")
return ref, nil, NewParserError(atmost(b, 5), "expected 'false'")
}
ref = p.builder.Push(ast.Node{
Kind: ast.Bool,
ref = p.builder.Push(Node{
Kind: Bool,
Data: b[:5],
})
@@ -324,7 +439,7 @@ func atmost(b []byte, n int) []byte {
return b[:n]
}
func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
v, rest, err := scanLiteralString(b)
if err != nil {
return nil, nil, nil, err
@@ -333,19 +448,20 @@ func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
return v, v[1 : len(v)-1], rest, nil
}
func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
// inline-table-open = %x7B ws ; {
// inline-table-close = ws %x7D ; }
// inline-table-sep = ws %x2C ws ; , Comma
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
parent := p.builder.Push(ast.Node{
Kind: ast.InlineTable,
parent := p.builder.Push(Node{
Kind: InlineTable,
Raw: p.Range(b[:1]),
})
first := true
var child ast.Reference
var child reference
b = b[1:]
@@ -356,7 +472,7 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return parent, nil, newDecodeError(previousB[:1], "inline table is incomplete")
return parent, nil, NewParserError(previousB[:1], "inline table is incomplete")
}
if b[0] == '}' {
@@ -371,7 +487,7 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b)
}
var kv ast.Reference
var kv reference
kv, b, err = p.parseKeyval(b)
if err != nil {
@@ -394,7 +510,7 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
}
//nolint:funlen,cyclop
func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
// array = array-open [ array-values ] ws-comment-newline array-close
// array-open = %x5B ; [
// array-close = %x5D ; ]
@@ -405,23 +521,39 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
arrayStart := b
b = b[1:]
parent := p.builder.Push(ast.Node{
Kind: ast.Array,
parent := p.builder.Push(Node{
Kind: Array,
})
// First indicates whether the parser is looking for the first element
// (non-comment) of the array.
first := true
var lastChild ast.Reference
lastChild := invalidReference
addChild := func(valueRef reference) {
if lastChild == invalidReference {
p.builder.AttachChild(parent, valueRef)
} else {
p.builder.Chain(lastChild, valueRef)
}
lastChild = valueRef
}
var err error
for len(b) > 0 {
b, err = p.parseOptionalWhitespaceCommentNewline(b)
cref := invalidReference
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
if len(b) == 0 {
return parent, nil, newDecodeError(arrayStart[:1], "array is incomplete")
return parent, nil, NewParserError(arrayStart[:1], "array is incomplete")
}
if b[0] == ']' {
@@ -430,16 +562,19 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
if b[0] == ',' {
if first {
return parent, nil, newDecodeError(b[0:1], "array cannot start with comma")
return parent, nil, NewParserError(b[0:1], "array cannot start with comma")
}
b = b[1:]
b, err = p.parseOptionalWhitespaceCommentNewline(b)
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
} else if !first {
return parent, nil, newDecodeError(b[0:1], "array elements must be separated by commas")
return parent, nil, NewParserError(b[0:1], "array elements must be separated by commas")
}
// TOML allows trailing commas in arrays.
@@ -447,23 +582,22 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
break
}
var valueRef ast.Reference
var valueRef reference
valueRef, b, err = p.parseVal(b)
if err != nil {
return parent, nil, err
}
if first {
p.builder.AttachChild(parent, valueRef)
} else {
p.builder.Chain(lastChild, valueRef)
}
lastChild = valueRef
addChild(valueRef)
b, err = p.parseOptionalWhitespaceCommentNewline(b)
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
first = false
}
@@ -472,15 +606,34 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
return parent, rest, err
}
func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error) {
func (p *Parser) parseOptionalWhitespaceCommentNewline(b []byte) (reference, []byte, error) {
rootCommentRef := invalidReference
latestCommentRef := invalidReference
addComment := func(ref reference) {
if rootCommentRef == invalidReference {
rootCommentRef = ref
} else if latestCommentRef == invalidReference {
p.builder.AttachChild(rootCommentRef, ref)
latestCommentRef = ref
} else {
p.builder.Chain(latestCommentRef, ref)
latestCommentRef = ref
}
}
for len(b) > 0 {
var err error
b = p.parseWhitespace(b)
if len(b) > 0 && b[0] == '#' {
_, b, err = scanComment(b)
var ref reference
ref, b, err = p.parseComment(b)
if err != nil {
return nil, err
return invalidReference, nil, err
}
if ref != invalidReference {
addComment(ref)
}
}
@@ -491,17 +644,17 @@ func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error)
if b[0] == '\n' || b[0] == '\r' {
b, err = p.parseNewline(b)
if err != nil {
return nil, err
return invalidReference, nil, err
}
} else {
break
}
}
return b, nil
return rootCommentRef, b, nil
}
func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) {
func (p *Parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) {
token, rest, err := scanMultilineLiteralString(b)
if err != nil {
return nil, nil, nil, err
@@ -520,7 +673,7 @@ func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte,
}
//nolint:funlen,gocognit,cyclop
func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, error) {
func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
@@ -549,13 +702,13 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
startIdx := i
endIdx := len(token) - len(`"""`)
if escaped < 0 {
if !escaped {
str := token[startIdx:endIdx]
verr := utf8TomlValidAlreadyEscaped(str)
verr := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
return token, str, rest, nil
}
return nil, nil, nil, newDecodeError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
}
var builder bytes.Buffer
@@ -578,6 +731,10 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
switch token[i+j] {
case ' ', '\t':
continue
case '\r':
if token[i+j+1] == '\n' {
continue
}
case '\n':
isLastNonWhitespaceOnLine = true
}
@@ -613,6 +770,8 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
builder.WriteByte('\r')
case 't':
builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u':
x, err := hexToRune(atmost(token[i+1:], 4), 4)
if err != nil {
@@ -629,13 +788,13 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
builder.WriteRune(x)
i += 8
default:
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], "invalid escaped character %#U", c)
}
i++
} else {
size := utf8ValidNext(token[i:])
size := characters.Utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
@@ -645,7 +804,7 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
return token, builder.Bytes(), rest, nil
}
func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseKey(b []byte) (reference, []byte, error) {
// key = simple-key / dotted-key
// simple-key = quoted-key / unquoted-key
//
@@ -656,11 +815,11 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
// dot-sep = ws %x2E ws ; . Period
raw, key, b, err := p.parseSimpleKey(b)
if err != nil {
return ast.InvalidReference, nil, err
return invalidReference, nil, err
}
ref := p.builder.Push(ast.Node{
Kind: ast.Key,
ref := p.builder.Push(Node{
Kind: Key,
Raw: p.Range(raw),
Data: key,
})
@@ -675,8 +834,8 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
return ref, nil, err
}
p.builder.PushAndChain(ast.Node{
Kind: ast.Key,
p.builder.PushAndChain(Node{
Kind: Key,
Raw: p.Range(raw),
Data: key,
})
@@ -688,14 +847,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
return ref, b, nil
}
func (p *parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
func (p *Parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
if len(b) == 0 {
return nil, nil, nil, NewParserError(b, "expected key but found none")
}
// simple-key = quoted-key / unquoted-key
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
// quoted-key = basic-string / literal-string
if len(b) == 0 {
return nil, nil, nil, newDecodeError(b, "key is incomplete")
}
switch {
case b[0] == '\'':
return p.parseLiteralString(b)
@@ -705,12 +864,12 @@ func (p *parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
key, rest = scanUnquotedKey(b)
return key, key, rest, nil
default:
return nil, nil, nil, newDecodeError(b[0:1], "invalid character at start of key: %c", b[0])
return nil, nil, nil, NewParserError(b[0:1], "invalid character at start of key: %c", b[0])
}
}
//nolint:funlen,cyclop
func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
@@ -736,13 +895,13 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// Fast path. If there is no escape sequence, the string should just be
// an UTF-8 encoded string, which is the same as Go. In that case,
// validate the string and return a direct reference to the buffer.
if escaped < 0 {
if !escaped {
str := token[startIdx:endIdx]
verr := utf8TomlValidAlreadyEscaped(str)
verr := characters.Utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
return token, str, rest, nil
}
return nil, nil, nil, newDecodeError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
}
i := startIdx
@@ -770,6 +929,8 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteByte('\r')
case 't':
builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u':
x, err := hexToRune(token[i+1:len(token)-1], 4)
if err != nil {
@@ -787,13 +948,13 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteRune(x)
i += 8
default:
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], "invalid escaped character %#U", c)
}
i++
} else {
size := utf8ValidNext(token[i:])
size := characters.Utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid character %#U", c)
return nil, nil, nil, NewParserError(token[i:i+1], "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
@@ -805,7 +966,7 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
func hexToRune(b []byte, length int) (rune, error) {
if len(b) < length {
return -1, newDecodeError(b, "unicode point needs %d character, not %d", length, len(b))
return -1, NewParserError(b, "unicode point needs %d character, not %d", length, len(b))
}
b = b[:length]
@@ -820,19 +981,19 @@ func hexToRune(b []byte, length int) (rune, error) {
case 'A' <= c && c <= 'F':
d = uint32(c - 'A' + 10)
default:
return -1, newDecodeError(b[i:i+1], "non-hex character")
return -1, NewParserError(b[i:i+1], "non-hex character")
}
r = r*16 + d
}
if r > unicode.MaxRune || 0xD800 <= r && r < 0xE000 {
return -1, newDecodeError(b, "escape sequence is invalid Unicode code point")
return -1, NewParserError(b, "escape sequence is invalid Unicode code point")
}
return rune(r), nil
}
func (p *parser) parseWhitespace(b []byte) []byte {
func (p *Parser) parseWhitespace(b []byte) []byte {
// ws = *wschar
// wschar = %x20 ; Space
// wschar =/ %x09 ; Horizontal tab
@@ -842,31 +1003,32 @@ func (p *parser) parseWhitespace(b []byte) []byte {
}
//nolint:cyclop
func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error) {
switch b[0] {
case 'i':
if !scanFollowsInf(b) {
return ast.InvalidReference, nil, newDecodeError(atmost(b, 3), "expected 'inf'")
return invalidReference, nil, NewParserError(atmost(b, 3), "expected 'inf'")
}
return p.builder.Push(ast.Node{
Kind: ast.Float,
return p.builder.Push(Node{
Kind: Float,
Data: b[:3],
Raw: p.Range(b[:3]),
}), b[3:], nil
case 'n':
if !scanFollowsNan(b) {
return ast.InvalidReference, nil, newDecodeError(atmost(b, 3), "expected 'nan'")
return invalidReference, nil, NewParserError(atmost(b, 3), "expected 'nan'")
}
return p.builder.Push(ast.Node{
Kind: ast.Float,
return p.builder.Push(Node{
Kind: Float,
Data: b[:3],
Raw: p.Range(b[:3]),
}), b[3:], nil
case '+', '-':
return p.scanIntOrFloat(b)
}
//nolint:gomnd
if len(b) < 3 {
return p.scanIntOrFloat(b)
}
@@ -884,24 +1046,14 @@ func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, err
if idx == 2 && c == ':' || (idx == 4 && c == '-') {
return p.scanDateTime(b)
}
break
}
return p.scanIntOrFloat(b)
}
func digitsToInt(b []byte) int {
x := 0
for _, d := range b {
x *= 10
x += int(d - '0')
}
return x
}
//nolint:gocognit,cyclop
func (p *parser) scanDateTime(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) scanDateTime(b []byte) (reference, []byte, error) {
// scans for contiguous characters in [0-9T:Z.+-], and up to one space if
// followed by a digit.
hasDate := false
@@ -944,33 +1096,33 @@ byteLoop:
}
}
var kind ast.Kind
var kind Kind
if hasTime {
if hasDate {
if hasTz {
kind = ast.DateTime
kind = DateTime
} else {
kind = ast.LocalDateTime
kind = LocalDateTime
}
} else {
kind = ast.LocalTime
kind = LocalTime
}
} else {
kind = ast.LocalDate
kind = LocalDate
}
return p.builder.Push(ast.Node{
return p.builder.Push(Node{
Kind: kind,
Data: b[:i],
}), b[i:], nil
}
//nolint:funlen,gocognit,cyclop
func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) {
i := 0
if len(b) > 2 && b[0] == '0' && b[1] != '.' && b[1] != 'e' {
if len(b) > 2 && b[0] == '0' && b[1] != '.' && b[1] != 'e' && b[1] != 'E' {
var isValidRune validRuneFn
switch b[1] {
@@ -993,9 +1145,10 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
}
}
return p.builder.Push(ast.Node{
Kind: ast.Integer,
return p.builder.Push(Node{
Kind: Integer,
Data: b[:i],
Raw: p.Range(b[:i]),
}), b[i:], nil
}
@@ -1016,42 +1169,45 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
if c == 'i' {
if scanFollowsInf(b[i:]) {
return p.builder.Push(ast.Node{
Kind: ast.Float,
return p.builder.Push(Node{
Kind: Float,
Data: b[:i+3],
Raw: p.Range(b[:i+3]),
}), b[i+3:], nil
}
return ast.InvalidReference, nil, newDecodeError(b[i:i+1], "unexpected character 'i' while scanning for a number")
return invalidReference, nil, NewParserError(b[i:i+1], "unexpected character 'i' while scanning for a number")
}
if c == 'n' {
if scanFollowsNan(b[i:]) {
return p.builder.Push(ast.Node{
Kind: ast.Float,
return p.builder.Push(Node{
Kind: Float,
Data: b[:i+3],
Raw: p.Range(b[:i+3]),
}), b[i+3:], nil
}
return ast.InvalidReference, nil, newDecodeError(b[i:i+1], "unexpected character 'n' while scanning for a number")
return invalidReference, nil, NewParserError(b[i:i+1], "unexpected character 'n' while scanning for a number")
}
break
}
if i == 0 {
return ast.InvalidReference, b, newDecodeError(b, "incomplete number")
return invalidReference, b, NewParserError(b, "incomplete number")
}
kind := ast.Integer
kind := Integer
if isFloat {
kind = ast.Float
kind = Float
}
return p.builder.Push(ast.Node{
return p.builder.Push(Node{
Kind: kind,
Data: b[:i],
Raw: p.Range(b[:i]),
}), b[i:], nil
}
@@ -1078,11 +1234,11 @@ func isValidBinaryRune(r byte) bool {
func expect(x byte, b []byte) ([]byte, error) {
if len(b) == 0 {
return nil, newDecodeError(b, "expected character %c but the document ended here", x)
return nil, NewParserError(b, "expected character %c but the document ended here", x)
}
if b[0] != x {
return nil, newDecodeError(b[0:1], "expected character %c", x)
return nil, NewParserError(b[0:1], "expected character %c", x)
}
return b[1:], nil
+629
View File
@@ -0,0 +1,629 @@
package unstable
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestParser_AST_Numbers(t *testing.T) {
examples := []struct {
desc string
input string
kind Kind
err bool
}{
{
desc: "integer just digits",
input: `1234`,
kind: Integer,
},
{
desc: "integer zero",
input: `0`,
kind: Integer,
},
{
desc: "integer sign",
input: `+99`,
kind: Integer,
},
{
desc: "integer hex uppercase",
input: `0xDEADBEEF`,
kind: Integer,
},
{
desc: "integer hex lowercase",
input: `0xdead_beef`,
kind: Integer,
},
{
desc: "integer octal",
input: `0o01234567`,
kind: Integer,
},
{
desc: "integer binary",
input: `0b11010110`,
kind: Integer,
},
{
desc: "float zero",
input: `0.0`,
kind: Float,
},
{
desc: "float positive zero",
input: `+0.0`,
kind: Float,
},
{
desc: "float negative zero",
input: `-0.0`,
kind: Float,
},
{
desc: "float pi",
input: `3.1415`,
kind: Float,
},
{
desc: "float negative",
input: `-0.01`,
kind: Float,
},
{
desc: "float signed exponent",
input: `5e+22`,
kind: Float,
},
{
desc: "float exponent lowercase",
input: `1e06`,
kind: Float,
},
{
desc: "float exponent uppercase",
input: `-2E-2`,
kind: Float,
},
{
desc: "float fractional with exponent",
input: `6.626e-34`,
kind: Float,
},
{
desc: "float underscores",
input: `224_617.445_991_228`,
kind: Float,
},
{
desc: "inf",
input: `inf`,
kind: Float,
},
{
desc: "inf negative",
input: `-inf`,
kind: Float,
},
{
desc: "inf positive",
input: `+inf`,
kind: Float,
},
{
desc: "nan",
input: `nan`,
kind: Float,
},
{
desc: "nan negative",
input: `-nan`,
kind: Float,
},
{
desc: "nan positive",
input: `+nan`,
kind: Float,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := Parser{}
p.Reset([]byte(`A = ` + e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
expected := astNode{
Kind: KeyValue,
Children: []astNode{
{Kind: e.kind, Data: []byte(e.input)},
{Kind: Key, Data: []byte(`A`)},
},
}
compareNode(t, expected, p.Expression())
}
})
}
}
type (
astNode struct {
Kind Kind
Data []byte
Children []astNode
}
)
func compareNode(t *testing.T, e astNode, n *Node) {
t.Helper()
require.Equal(t, e.Kind, n.Kind)
require.Equal(t, e.Data, n.Data)
compareIterator(t, e.Children, n.Children())
}
func compareIterator(t *testing.T, expected []astNode, actual Iterator) {
t.Helper()
idx := 0
for actual.Next() {
n := actual.Node()
if idx >= len(expected) {
t.Fatal("extra child in actual tree")
}
e := expected[idx]
compareNode(t, e, n)
idx++
}
if idx < len(expected) {
t.Fatal("missing children in actual", "idx =", idx, "expected =", len(expected))
}
}
//nolint:funlen
func TestParser_AST(t *testing.T) {
examples := []struct {
desc string
input string
ast astNode
err bool
}{
{
desc: "simple string assignment",
input: `A = "hello"`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: String,
Data: []byte(`hello`),
},
{
Kind: Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "simple bool assignment",
input: `A = true`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: Bool,
Data: []byte(`true`),
},
{
Kind: Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "array of strings",
input: `A = ["hello", ["world", "again"]]`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: Array,
Children: []astNode{
{
Kind: String,
Data: []byte(`hello`),
},
{
Kind: Array,
Children: []astNode{
{
Kind: String,
Data: []byte(`world`),
},
{
Kind: String,
Data: []byte(`again`),
},
},
},
},
},
{
Kind: Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "array of arrays of strings",
input: `A = ["hello", "world"]`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: Array,
Children: []astNode{
{
Kind: String,
Data: []byte(`hello`),
},
{
Kind: String,
Data: []byte(`world`),
},
},
},
{
Kind: Key,
Data: []byte(`A`),
},
},
},
},
{
desc: "inline table",
input: `name = { first = "Tom", last = "Preston-Werner" }`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Preston-Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := Parser{}
p.Reset([]byte(e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
compareNode(t, e.ast, p.Expression())
}
})
}
}
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
p := &Parser{}
b.Run("4", func(b *testing.B) {
input := []byte(`"\u1234\u5678\u9ABC\u1234\u5678\u9ABC"`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for i := 0; i < b.N; i++ {
p.parseBasicString(input)
}
})
b.Run("8", func(b *testing.B) {
input := []byte(`"\u12345678\u9ABCDEF0\u12345678\u9ABCDEF0"`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for i := 0; i < b.N; i++ {
p.parseBasicString(input)
}
})
}
func BenchmarkParseBasicStringsEasy(b *testing.B) {
p := &Parser{}
for _, size := range []int{1, 4, 8, 16, 21} {
b.Run(strconv.Itoa(size), func(b *testing.B) {
input := []byte(`"` + strings.Repeat("A", size) + `"`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for i := 0; i < b.N; i++ {
p.parseBasicString(input)
}
})
}
}
func TestParser_AST_DateTimes(t *testing.T) {
examples := []struct {
desc string
input string
kind Kind
err bool
}{
{
desc: "offset-date-time with delim 'T' and UTC offset",
input: `2021-07-21T12:08:05Z`,
kind: DateTime,
},
{
desc: "offset-date-time with space delim and +8hours offset",
input: `2021-07-21 12:08:05+08:00`,
kind: DateTime,
},
{
desc: "local-date-time with nano second",
input: `2021-07-21T12:08:05.666666666`,
kind: LocalDateTime,
},
{
desc: "local-date-time",
input: `2021-07-21T12:08:05`,
kind: LocalDateTime,
},
{
desc: "local-date",
input: `2021-07-21`,
kind: LocalDate,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := Parser{}
p.Reset([]byte(`A = ` + e.input))
p.NextExpression()
err := p.Error()
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
expected := astNode{
Kind: KeyValue,
Children: []astNode{
{Kind: e.kind, Data: []byte(e.input)},
{Kind: Key, Data: []byte(`A`)},
},
}
compareNode(t, expected, p.Expression())
}
})
}
}
// This example demonstrates how to parse a TOML document and preserving
// comments. Comments are stored in the AST as Comment nodes. This example
// displays the structure of the full AST generated by the parser using the
// following structure:
//
// 1. Each root-level expression is separated by three dashes.
// 2. Bytes associated to a node are displayed in square brackets.
// 3. Siblings have the same indentation.
// 4. Children of a node are indented one level.
func ExampleParser_comments() {
doc := `# Top of the document comment.
# Optional, any amount of lines.
# Above table.
[table] # Next to table.
# Above simple value.
key = "value" # Next to simple value.
# Below simple value.
# Some comment alone.
# Multiple comments, on multiple lines.
# Above inline table.
name = { first = "Tom", last = "Preston-Werner" } # Next to inline table.
# Below inline table.
# Above array.
array = [ 1, 2, 3 ] # Next to one-line array.
# Below array.
# Above multi-line array.
key5 = [ # Next to start of inline array.
# Second line before array content.
1, # Next to first element.
# After first element.
# Before second element.
2,
3, # Next to last element
# After last element.
] # Next to end of array.
# Below multi-line array.
# Before array table.
[[products]] # Next to array table.
# After array table.
`
var printGeneric func(*Parser, int, *Node)
printGeneric = func(p *Parser, indent int, e *Node) {
if e == nil {
return
}
s := p.Shape(e.Raw)
x := fmt.Sprintf("%d:%d->%d:%d (%d->%d)", s.Start.Line, s.Start.Column, s.End.Line, s.End.Column, s.Start.Offset, s.End.Offset)
fmt.Printf("%-25s | %s%s [%s]\n", x, strings.Repeat(" ", indent), e.Kind, e.Data)
printGeneric(p, indent+1, e.Child())
printGeneric(p, indent, e.Next())
}
printTree := func(p *Parser) {
for p.NextExpression() {
e := p.Expression()
fmt.Println("---")
printGeneric(p, 0, e)
}
if err := p.Error(); err != nil {
panic(err)
}
}
p := &Parser{
KeepComments: true,
}
p.Reset([]byte(doc))
printTree(p)
// Output:
// ---
// 1:1->1:31 (0->30) | Comment [# Top of the document comment.]
// ---
// 2:1->2:33 (31->63) | Comment [# Optional, any amount of lines.]
// ---
// 4:1->4:15 (65->79) | Comment [# Above table.]
// ---
// 1:1->1:1 (0->0) | Table []
// 5:2->5:7 (81->86) | Key [table]
// 5:9->5:25 (88->104) | Comment [# Next to table.]
// ---
// 6:1->6:22 (105->126) | Comment [# Above simple value.]
// ---
// 1:1->1:1 (0->0) | KeyValue []
// 7:7->7:14 (133->140) | String [value]
// 7:1->7:4 (127->130) | Key [key]
// 7:15->7:38 (141->164) | Comment [# Next to simple value.]
// ---
// 8:1->8:22 (165->186) | Comment [# Below simple value.]
// ---
// 10:1->10:22 (188->209) | Comment [# Some comment alone.]
// ---
// 12:1->12:40 (211->250) | Comment [# Multiple comments, on multiple lines.]
// ---
// 14:1->14:22 (252->273) | Comment [# Above inline table.]
// ---
// 1:1->1:1 (0->0) | KeyValue []
// 15:8->15:9 (281->282) | InlineTable []
// 1:1->1:1 (0->0) | KeyValue []
// 15:18->15:23 (291->296) | String [Tom]
// 15:10->15:15 (283->288) | Key [first]
// 1:1->1:1 (0->0) | KeyValue []
// 15:32->15:48 (305->321) | String [Preston-Werner]
// 15:25->15:29 (298->302) | Key [last]
// 15:1->15:5 (274->278) | Key [name]
// 15:51->15:74 (324->347) | Comment [# Next to inline table.]
// ---
// 16:1->16:22 (348->369) | Comment [# Below inline table.]
// ---
// 18:1->18:15 (371->385) | Comment [# Above array.]
// ---
// 1:1->1:1 (0->0) | KeyValue []
// 1:1->1:1 (0->0) | Array []
// 19:11->19:12 (396->397) | Integer [1]
// 19:14->19:15 (399->400) | Integer [2]
// 19:17->19:18 (402->403) | Integer [3]
// 19:1->19:6 (386->391) | Key [array]
// 19:21->19:46 (406->431) | Comment [# Next to one-line array.]
// ---
// 20:1->20:15 (432->446) | Comment [# Below array.]
// ---
// 22:1->22:26 (448->473) | Comment [# Above multi-line array.]
// ---
// 1:1->1:1 (0->0) | KeyValue []
// 1:1->1:1 (0->0) | Array []
// 23:10->23:42 (483->515) | Comment [# Next to start of inline array.]
// 24:3->24:38 (518->553) | Comment [# Second line before array content.]
// 25:3->25:4 (556->557) | Integer [1]
// 25:6->25:30 (559->583) | Comment [# Next to first element.]
// 26:3->26:25 (586->608) | Comment [# After first element.]
// 27:3->27:27 (611->635) | Comment [# Before second element.]
// 28:3->28:4 (638->639) | Integer [2]
// 29:3->29:4 (643->644) | Integer [3]
// 29:6->29:28 (646->668) | Comment [# Next to last element]
// 30:3->30:24 (671->692) | Comment [# After last element.]
// 23:1->23:5 (474->478) | Key [key5]
// 31:3->31:26 (695->718) | Comment [# Next to end of array.]
// ---
// 32:1->32:26 (719->744) | Comment [# Below multi-line array.]
// ---
// 34:1->34:22 (746->767) | Comment [# Before array table.]
// ---
// 1:1->1:1 (0->0) | ArrayTable []
// 35:3->35:11 (770->778) | Key [products]
// 35:14->35:36 (781->803) | Comment [# Next to array table.]
// ---
// 36:1->36:21 (804->824) | Comment [# After array table.]
}
func ExampleParser() {
doc := `
hello = "world"
value = 42
`
p := Parser{}
p.Reset([]byte(doc))
for p.NextExpression() {
e := p.Expression()
fmt.Printf("Expression: %s\n", e.Kind)
value := e.Value()
it := e.Key()
k := it.Node() // shortcut: we know there is no dotted key in the example
fmt.Printf("%s -> (%s) %s\n", k.Data, value.Kind, value.Data)
}
// Output:
// Expression: KeyValue
// hello -> (String) world
// Expression: KeyValue
// value -> (Integer) 42
}
+72 -77
View File
@@ -1,4 +1,6 @@
package toml
package unstable
import "github.com/pelletier/go-toml/v2/internal/characters"
func scanFollows(b []byte, pattern string) bool {
n := len(pattern)
@@ -53,17 +55,17 @@ func scanLiteralString(b []byte) ([]byte, []byte, error) {
switch b[i] {
case '\'':
return b[:i+1], b[i+1:], nil
case '\n':
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines")
case '\n', '\r':
return nil, nil, NewParserError(b[i:i+1], "literal strings cannot have new lines")
}
size := utf8ValidNext(b[i:])
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, newDecodeError(b[i:i+1], "invalid character")
return nil, nil, NewParserError(b[i:i+1], "invalid character")
}
i += size
}
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string")
return nil, nil, NewParserError(b[len(b):], "unterminated literal string")
}
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
@@ -76,49 +78,61 @@ func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
// mll-quotes = 1*2apostrophe
for i := 3; i < len(b); {
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
i += 3
switch b[i] {
case '\'':
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
i += 3
// At that point we found 3 apostrophe, and i is the
// index of the byte after the third one. The scanner
// needs to be eager, because there can be an extra 2
// apostrophe that can be accepted at the end of the
// string.
// At that point we found 3 apostrophe, and i is the
// index of the byte after the third one. The scanner
// needs to be eager, because there can be an extra 2
// apostrophe that can be accepted at the end of the
// string.
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
i++
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
i++
if i < len(b) && b[i] == '\'' {
return nil, nil, NewParserError(b[i-3:i+1], "''' not allowed in multiline literal string")
}
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
i++
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
case '\r':
if len(b) < i+2 {
return nil, nil, NewParserError(b[len(b):], `need a \n after \r`)
}
i++
if i < len(b) && b[i] == '\'' {
return nil, nil, newDecodeError(b[i-3:i+1], "''' not allowed in multiline literal string")
if b[i+1] != '\n' {
return nil, nil, NewParserError(b[i:i+2], `need a \n after \r`)
}
return b[:i], b[i:], nil
i += 2 // skip the \n
continue
}
size := utf8ValidNext(b[i:])
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, newDecodeError(b[i:i+1], "invalid character")
return nil, nil, NewParserError(b[i:i+1], "invalid character")
}
i += size
}
return nil, nil, newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
return nil, nil, NewParserError(b[len(b):], `multiline literal string not terminated by '''`)
}
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
const lenCRLF = 2
if len(b) < lenCRLF {
return nil, nil, newDecodeError(b, "windows new line expected")
return nil, nil, NewParserError(b, "windows new line expected")
}
if b[1] != '\n' {
return nil, nil, newDecodeError(b, `windows new line should be \r\n`)
return nil, nil, NewParserError(b, `windows new line should be \r\n`)
}
return b[:lenCRLF], b[lenCRLF:], nil
@@ -137,7 +151,6 @@ func scanWhitespace(b []byte) ([]byte, []byte) {
return b, b[len(b):]
}
//nolint:unparam
func scanComment(b []byte) ([]byte, []byte, error) {
// comment-start-symbol = %x23 ; #
// non-ascii = %x80-D7FF / %xE000-10FFFF
@@ -149,9 +162,15 @@ func scanComment(b []byte) ([]byte, []byte, error) {
if b[i] == '\n' {
return b[:i], b[i:], nil
}
size := utf8ValidNext(b[i:])
if b[i] == '\r' {
if i+1 < len(b) && b[i+1] == '\n' {
return b[:i+1], b[i+1:], nil
}
return nil, nil, NewParserError(b[i:i+1], "invalid character in comment")
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, newDecodeError(b[i:i+1], "invalid character in comment")
return nil, nil, NewParserError(b[i:i+1], "invalid character in comment")
}
i += size
@@ -160,50 +179,34 @@ func scanComment(b []byte) ([]byte, []byte, error) {
return b, b[len(b):], nil
}
func scanBasicString(b []byte) ([]byte, int, []byte, error) {
func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// escaped = escape escape-seq-char
escaped := -1 // index of the first \. -1 means no escape character in there.
escaped := false
i := 1
loop:
for ; i < len(b); i++ {
switch b[i] {
case '"':
return b[:i+1], escaped, b[i+1:], nil
case '\n':
return nil, escaped, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines")
case '\n', '\r':
return nil, escaped, nil, NewParserError(b[i:i+1], "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, newDecodeError(b[i:i+1], "need a character after \\")
}
escaped = i
i += 2 // skip the next character
break loop
}
}
for ; i < len(b); i++ {
switch b[i] {
case '"':
return b[:i+1], escaped, b[i+1:], nil
case '\n':
return nil, escaped, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, newDecodeError(b[i:i+1], "need a character after \\")
return nil, escaped, nil, NewParserError(b[i:i+1], "need a character after \\")
}
escaped = true
i++ // skip the next character
}
}
return nil, escaped, nil, newDecodeError(b[len(b):], `basic string not terminated by "`)
return nil, escaped, nil, NewParserError(b[len(b):], `basic string not terminated by "`)
}
func scanMultilineBasicString(b []byte) ([]byte, int, []byte, error) {
func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
@@ -215,10 +218,9 @@ func scanMultilineBasicString(b []byte) ([]byte, int, []byte, error) {
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline )
escaped := -1
escaped := false
i := 3
loop:
for ; i < len(b); i++ {
switch b[i] {
case '"':
@@ -242,34 +244,27 @@ loop:
i++
if i < len(b) && b[i] == '"' {
return nil, escaped, nil, newDecodeError(b[i-3:i+1], `""" not allowed in multiline basic string`)
return nil, escaped, nil, NewParserError(b[i-3:i+1], `""" not allowed in multiline basic string`)
}
return b[:i], escaped, b[i:], nil
}
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, newDecodeError(b[len(b):], "need a character after \\")
}
escaped = i
i += 2 // skip the next character
break loop
}
}
for ; i < len(b); i++ {
switch b[i] {
case '"':
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
return b[:i+3], escaped, b[i+3:], nil
}
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, newDecodeError(b[len(b):], "need a character after \\")
return nil, escaped, nil, NewParserError(b[len(b):], "need a character after \\")
}
escaped = true
i++ // skip the next character
case '\r':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[len(b):], `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, escaped, nil, NewParserError(b[i:i+2], `need a \n after \r`)
}
i++ // skip the \n
}
}
return nil, escaped, nil, newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
return nil, escaped, nil, NewParserError(b[len(b):], `multiline basic string not terminated by """`)
}
+7
View File
@@ -0,0 +1,7 @@
package unstable
// The Unmarshaler interface may be implemented by types to customize their
// behavior when being unmarshaled from a TOML document.
type Unmarshaler interface {
UnmarshalTOML(value *Node) error
}