Compare commits

...

156 Commits

Author SHA1 Message Date
Thomas Pelletier b371733c67 Make all nodes contain Raw 2022-08-22 21:05:41 -04:00
Thomas Pelletier 64dcce07ea WIP 2022-08-22 23:04:44 +00: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
Thomas Pelletier 19751e8a51 Missing performance section 2021-10-28 19:10:43 -04:00
Thomas Pelletier 925f214125 Add GitHub release configuration (#644) 2021-10-28 19:06:14 -04:00
Thomas Pelletier 39f893ad99 Multiline strings fixes (#643)
* scanner: allow multiline strings to end with "" or ''

* parser: trim all whitespaces after \ in multiline
2021-10-28 18:26:34 -04:00
Thomas Pelletier c871a61015 unmarshal: use UnmarshalText for any type (#642)
Not only structs can implement TextUnmarshaler.

Fixes #564
2021-10-28 17:02:47 -04:00
Thomas Pelletier d0d001625c unmarshal: don't panic when storing table in slice (#641)
New error message:

```
toml: cannot store a table in a slice
1| [things]
 |  ~~~~~~ cannot store a table in a slice
2| foo = "bar"
```

Fixes #623
2021-10-25 16:47:10 -04:00
Thomas Pelletier 64941b99e2 unmarshal: empty document results in map (#640)
Fixes #602
2021-10-25 15:55:54 -04:00
Thomas Pelletier ed02a1f192 seen: check for explicit tables on dotted keys (#639)
The TOML spec is being clarified to say that dotted keys "define" their
intermediate tables. Therefore the seen tracker needs to verify that none of
them reference an explicit table.

Also added a missing seen expression check for key-values parsed as part of a
table section.

See https://github.com/toml-lang/toml/issues/846
2021-10-22 23:25:28 -04:00
Thomas Pelletier 4d7c9ddac7 Floats and integers parsing fixes (#638)
* parser: fix scan of float with exp but no decimal
* decoder: validate leading zeros for decimals
2021-10-22 22:25:56 -04:00
Thomas Pelletier feb1830dcc tomltest: enable TestTOMLTest_Valid_Comment_Tricky 2021-10-21 22:30:58 -04:00
Thomas Pelletier 1c33d6ce20 tomltest: custom comparison functions (#637) 2021-10-21 22:29:04 -04:00
Thomas Pelletier 3000471a12 parser: improve floats validation (#636) 2021-10-20 08:49:28 -04:00
Thomas Pelletier 1f33a6a476 tomltest: enable Valid_Datetime_LocalTime 2021-10-19 21:20:45 -04:00
Thomas Pelletier 2700aad5d2 tomltest: run UTF8 tests (#634) 2021-10-19 16:00:56 -04:00
Thomas Pelletier 7ccaa2744e tomltest: unmarshal JSONs for tests (#633)
Comparing the output and the expected results byte-wise means we get false
negative when order doesn't matter (for example the ValidTableKeyword test).
2021-10-19 15:29:49 -04:00
Johanan Idicula df4bb061f8 time: follow RFC3339 spec for datetime (#632) 2021-10-18 09:56:07 -04:00
Thomas Pelletier 9e81ce1c33 Create SECURITY.md 2021-10-17 20:57:34 -04:00
Cameron Moore a23850f29b decode: preserve nanosecond precision when decoding time (#626)
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-10-17 20:43:29 -04:00
Johanan Idicula 76f53c857b unmarshal: validate date (#622) 2021-10-17 20:18:20 -04:00
Thomas Pelletier 85f5d567e4 parser: validate invalid ASCII control characters 2021-10-16 07:41:12 -04:00
Thomas Pelletier bd5cba0b0b Update benchmarks readme (#630)
* Fix ci.sh for new benchmarks

Nice + taskset are more stable on my machine. We want to excude non high-level
benchmarks. BurntSushi/toml now supports canada.toml.

* Update latest benchmarks in README
2021-10-15 19:53:40 -04:00
Thomas Pelletier cd54472d03 Validate UTF-8 (#629) 2021-10-15 19:13:21 -04:00
Thomas Pelletier cc0d1a90ff testgen: skip currently failing tests (#627) 2021-10-14 11:14:44 -04:00
Sterling Hanenkamp 4984dcb5e9 encode: ensure floats have decimal point (#615)
Fixes #571

Co-authored-by: Sterling Hanenkamp <sterling@ziprecruiter.com>
2021-10-14 08:34:54 -04:00
jidicula 86632bc190 parser: fail when missing array separator (#616)
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-10-14 08:26:29 -04:00
Cameron Moore d25eec183f gotoml-test-decoder: add toml-test decoder command (#619) 2021-10-14 08:14:34 -04:00
Riya John e96746311c decoder: fix panic date time should have a timezone (#614)
Fixes #596

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-10-06 21:24:25 -04:00
Cameron Moore 62acca2b68 tomltestgen: add toml-test unit test generation command (#610)
Tests are hidden behind a "testsuite" build tag for now since many tests
are failing.  Use `go test -tags testsuite` to activate.

Use `go generate` to regenerate toml_testgen_test.go.

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-10-03 22:15:30 -04:00
Cameron Moore 476492a85c unmarshal: support lowercase 'T' and 'Z' in date-time parsing (#601)
RFC3399 allows for lowercase 't' and 'z' in date-time values.

Fixes #600
2021-09-25 10:02:23 -07:00
Thomas Pelletier ee9b902222 unmarshal: convert ints if target type is compatible (#594)
This is required to support custom types.

Fixes #590
2021-09-09 21:25:14 -04:00
Thomas Pelletier fa56f48daf parser: don't overflow when parsing bad times (#593)
Fixes #585
2021-09-09 11:59:37 -04:00
Thomas Pelletier f34c9c332f scanner: fix error reporting for last comments (#591)
When an invalid TOML expression ends with a comment before the end of
file, the decode error would take a nil from scanComment, which is not
part of the document.

Fixes #588
2021-09-08 21:54:30 -04:00
Thomas Pelletier a0d685d482 unmarshal: don't crash on unterminated inline table (#587)
Fixes #586
2021-09-07 20:08:59 -04:00
Thomas Pelletier 4a5ae9e81e errors: fix context generation with only one line 2021-09-07 10:36:22 -04:00
Thomas Pelletier 7e2fa1bc80 unmarshal: fix non-terminated array error
Fixes #581
2021-09-07 10:36:22 -04:00
Thomas Pelletier 40cfb6f458 parser: don't crash on unterminated table key (#580)
* parser: don't crash on unterminated table key

Fixes #579

* parser: fix format of error returned by expect

EOF was missing the format string and %U is not very human friendly.
2021-09-06 12:18:45 -04:00
Thomas Pelletier 1230ca485e unmarshal: make copy of non addressable values (#576)
When unmarshaling into a nested struct in a map, the value is not
addressable. In that case, make a copy of it and modify it instead.

Fixes #575
2021-08-31 20:22:38 -04:00
Thomas Pelletier 69ab7e10d1 Go 1.17 release (#574)
Minimum supported version: Go 1.16.
2021-08-17 09:43:52 -04:00
Thomas Pelletier fa07960695 Add installation instructions (#572) 2021-07-27 18:12:44 -04:00
kkHAIKE 8be357dfa1 Add LocalTime to interface{} decode support (#567)
Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-07-21 17:50:12 +02:00
kkHAIKE a93b34d984 Unicode parsing optimization (#568)
Inline call to hexToRune and uses specialized parsing, as found in encoding/json.

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
2021-07-21 10:50:03 +02:00
Matthieu MOREL 9c24fbeaad Set up Dependabot for GitHub actions and docker (#570) 2021-07-20 16:54:26 +02:00
Thomas Pelletier f6b38c33b7 Provide own implementation of Local* (#558)
* Reduces the public API.
* Reuses optimized parsing functions.
* Removes reliance on Google code under Apache license.
2021-06-08 20:27:05 -04:00
Thomas Pelletier 773f10110c Unmarshal recursive structs (#557)
Co-authored-by: Nabetani <takenori@nabetani.sakura.ne.jp>
2021-06-08 14:22:39 -04:00
Thomas Pelletier 618f0181ac AST Tweaks (#551)
* Use pointers instead of copying around ast.Node

Node is a 56B struct that is constantly in the hot path. Passing nodes
around by copy had a cost that started to add up. This change replaces
them by pointers. Using unsafe pointer arithmetic and converting
sibling/child indexes to relative offsets, it removes the need to carry
around a pointer to the root of the tree. This saves 8B per Node. This
space will be used to store an extra []byte slice to provide contextual
error handling on all nodes, including the ones whose data is different
than the raw input (for example: strings with escaped characters), while
staying under the size of a cache line.

* Remove conditional

* Add Raw to track range in data for parsed values

* Simplify reference tracking
2021-06-03 21:48:51 -04:00
Thomas Pelletier f3bb20ea79 Benchmark marshal (#550) 2021-06-02 09:29:19 -04:00
Thomas Pelletier b0d6c62255 Don't use bytes.Buffer when not necessary (#549)
When parsing strings, they can be referenced directly from the document
when they don't contain escaped characters. This avoids paying to cost
of allocating (and sometimes growing) the bytes buffer unecessarily.
2021-06-01 09:51:59 -04:00
Thomas Pelletier b202375414 Add benchmarks results to readme (#548) 2021-06-01 09:10:17 -04:00
Thomas Pelletier 250e073408 Stack-based unmarshaler (#546)
* Benchmark script

* Rewrite unmarshaler using the stack

Instead of tracking the build chain using `target`s, use the stack
instead.

Working and most benchmarks look good, but regression on structs unmarshalling.

~60% slower on ReferenceFile/struct.

* Shortcut to check if last node of iterator

* Remove unecessary pointer allocation

* Skip over unused keys without marking them as seen

* Add some tests

* Fix mktemp on macos
2021-05-31 12:14:13 -04:00
Thomas Pelletier 11f022ab09 Fix parser skipping over the whole file (#547)
* Add test for ReferenceFile/struct
* Stop skipping after table
* Add .gitattributes to force LF encoding
* Fix the reference file
2021-05-30 18:53:07 -04:00
Thomas Pelletier 840df4a229 seen tracker: use array for storage (#545)
name                              old time/op    new time/op    delta
UnmarshalDataset/config-32          81.2ms ± 3%    77.8ms ± 3%   -4.25%  (p=0.000 n=20+19)
UnmarshalDataset/canada-32           104ms ± 5%     105ms ± 4%     ~     (p=0.102 n=20+20)
UnmarshalDataset/citm_catalog-32    57.5ms ± 5%    59.0ms ± 5%   +2.54%  (p=0.033 n=20+20)
UnmarshalDataset/twitter-32         25.7ms ± 7%    28.1ms ± 5%   +9.33%  (p=0.000 n=20+20)
UnmarshalDataset/code-32             305ms ± 6%     292ms ± 5%   -4.29%  (p=0.000 n=20+19)
UnmarshalDataset/example-32          519µs ± 6%     522µs ± 5%     ~     (p=0.659 n=20+20)
UnmarshalSimple/struct-32           1.44µs ± 1%    1.17µs ± 6%  -18.78%  (p=0.000 n=14+20)
UnmarshalSimple/map-32              2.30µs ± 4%    1.99µs ± 4%  -13.65%  (p=0.000 n=20+19)
ReferenceFile/struct-32             44.1µs ± 4%    38.1µs ± 5%  -13.61%  (p=0.000 n=18+20)
ReferenceFile/map-32                 197µs ± 7%     189µs ± 5%   -3.91%  (p=0.000 n=20+20)
HugoFrontMatter-32                  45.9µs ± 6%    39.3µs ± 6%  -14.46%  (p=0.000 n=19+20)

name                              old speed      new speed      delta
UnmarshalDataset/config-32        12.9MB/s ± 3%  13.5MB/s ± 3%   +4.42%  (p=0.000 n=20+19)
UnmarshalDataset/canada-32        21.1MB/s ± 5%  20.9MB/s ± 4%     ~     (p=0.101 n=20+20)
UnmarshalDataset/citm_catalog-32  9.72MB/s ± 6%  9.47MB/s ± 5%   -2.53%  (p=0.031 n=20+20)
UnmarshalDataset/twitter-32       17.2MB/s ± 7%  15.8MB/s ± 5%   -8.57%  (p=0.000 n=20+20)
UnmarshalDataset/code-32          8.81MB/s ± 7%  9.20MB/s ± 5%   +4.47%  (p=0.000 n=20+19)
UnmarshalDataset/example-32       15.6MB/s ± 6%  15.5MB/s ± 5%     ~     (p=0.644 n=20+20)
UnmarshalSimple/struct-32         7.61MB/s ± 1%  9.39MB/s ± 7%  +23.33%  (p=0.000 n=15+20)
UnmarshalSimple/map-32            4.78MB/s ± 4%  5.54MB/s ± 5%  +15.85%  (p=0.000 n=20+19)
ReferenceFile/struct-32            119MB/s ± 4%   138MB/s ± 5%  +15.79%  (p=0.000 n=18+20)
ReferenceFile/map-32              26.6MB/s ± 7%  27.7MB/s ± 5%   +4.06%  (p=0.000 n=20+20)
HugoFrontMatter-32                11.9MB/s ± 6%  13.9MB/s ± 6%  +16.91%  (p=0.000 n=19+20)

name                              old alloc/op   new alloc/op   delta
UnmarshalDataset/config-32          16.9MB ± 0%    13.9MB ± 0%  -17.48%  (p=0.000 n=18+18)
UnmarshalDataset/canada-32          74.3MB ± 0%    74.3MB ± 0%   -0.00%  (p=0.000 n=20+20)
UnmarshalDataset/citm_catalog-32    37.3MB ± 0%    37.3MB ± 0%   +0.11%  (p=0.000 n=20+20)
UnmarshalDataset/twitter-32         15.6MB ± 0%    15.6MB ± 0%     ~     (p=0.211 n=19+20)
UnmarshalDataset/code-32            59.5MB ± 0%    52.4MB ± 0%  -11.96%  (p=0.000 n=19+20)
UnmarshalDataset/example-32          238kB ± 0%     239kB ± 0%   +0.02%  (p=0.000 n=18+20)
UnmarshalSimple/struct-32             981B ± 0%      709B ± 0%  -27.73%  (p=0.000 n=20+20)
UnmarshalSimple/map-32              1.45kB ± 0%    1.17kB ± 0%  -18.82%  (p=0.000 n=20+20)
ReferenceFile/struct-32             11.8kB ± 0%     9.7kB ± 0%  -17.64%  (p=0.000 n=20+20)
ReferenceFile/map-32                51.5kB ± 0%    52.2kB ± 0%   +1.30%  (p=0.000 n=20+17)
HugoFrontMatter-32                  12.1kB ± 0%    11.1kB ± 0%   -7.97%  (p=0.000 n=20+19)

name                              old allocs/op  new allocs/op  delta
UnmarshalDataset/config-32            645k ± 0%      557k ± 0%  -13.76%  (p=0.000 n=20+16)
UnmarshalDataset/canada-32            896k ± 0%      896k ± 0%   -0.00%  (p=0.000 n=20+20)
UnmarshalDataset/citm_catalog-32      380k ± 0%      377k ± 0%   -0.75%  (p=0.000 n=19+20)
UnmarshalDataset/twitter-32           158k ± 0%      158k ± 0%   -0.01%  (p=0.000 n=18+18)
UnmarshalDataset/code-32             2.92M ± 0%     2.57M ± 0%  -11.87%  (p=0.000 n=20+20)
UnmarshalDataset/example-32          3.66k ± 0%     3.64k ± 0%   -0.63%  (p=0.000 n=20+20)
UnmarshalSimple/struct-32             13.0 ± 0%      10.0 ± 0%  -23.08%  (p=0.000 n=20+20)
UnmarshalSimple/map-32                22.0 ± 0%      19.0 ± 0%  -13.64%  (p=0.000 n=20+20)
ReferenceFile/struct-32                253 ± 0%       155 ± 0%  -38.74%  (p=0.000 n=20+20)
ReferenceFile/map-32                 1.67k ± 0%     1.44k ± 0%  -14.23%  (p=0.000 n=20+20)
HugoFrontMatter-32                     357 ± 0%       313 ± 0%  -12.32%  (p=0.000 n=20+20)
2021-05-26 18:47:00 -04:00
Thomas Pelletier c2d1fd86e5 Fix timezone detection when time has fractional component (#544) 2021-05-21 09:37:43 -04:00
Thomas Pelletier 238a6fef7d Add links to proper feedback channels 2021-05-15 08:55:07 -04:00
Thomas Pelletier 67852cf007 Clarify default struct tag being unsupported (#543)
Follow up to https://github.com/pelletier/go-toml/issues/542
2021-05-15 08:49:15 -04:00
Thomas Pelletier d276c42adc Run coverage test on branches only 2021-05-10 20:22:12 -04:00
80 changed files with 9593 additions and 3887 deletions
+4
View File
@@ -0,0 +1,4 @@
* text=auto
benchmark/benchmark.toml text eol=lf
testdata/** text eol=lf
+15 -4
View File
@@ -1,6 +1,17 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "gomod" - package-ecosystem: gomod
directory: "/" # Location of package manifests directory: /
schedule: schedule:
interval: "daily" interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: docker
directory: /
schedule:
interval: daily
open-pull-requests-limit: 10
+21
View File
@@ -0,0 +1,21 @@
changelog:
exclude:
labels:
- build
- testing
categories:
- title: What's new
labels:
- feature
- title: Performance
labels:
- performance
- title: Fixed bugs
labels:
- bug
- title: Documentation
labels:
- doc
- title: Other changes
labels:
- "*"
+4 -4
View File
@@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2
+3 -6
View File
@@ -1,23 +1,20 @@
name: coverage name: coverage
on: on:
push:
branches:
- v2
pull_request: pull_request:
branches: branches:
- v2 - v2
jobs: jobs:
report: report:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
name: report name: report
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup go - name: Setup go
uses: actions/setup-go@master uses: actions/setup-go@master
with: with:
go-version: 1.16 go-version: 1.19
- name: Run tests with coverage - name: Run tests with coverage
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}" run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
+39
View File
@@ -0,0 +1,39 @@
name: release
on:
push:
tags:
- "v2.*"
workflow_call:
inputs:
args:
description: "Extra arguments to pass goreleaser"
default: ""
required: false
type: string
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
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 }}
+9 -2
View File
@@ -12,14 +12,21 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest'] os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
go: [ '1.15', '1.16' ] go: [ '1.18', '1.19' ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }} name: ${{ matrix.go }}/${{ matrix.os }}
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup go ${{ matrix.go }} - name: Setup go ${{ matrix.go }}
uses: actions/setup-go@master uses: actions/setup-go@master
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run unit tests - name: Run unit tests
run: go test -race ./... run: go test -race ./...
release-check:
if: ${{ github.ref != 'refs/heads/v2' }}
uses: pelletier/go-toml/.github/workflows/release.yml@v2
with:
args: --snapshot
+1
View File
@@ -3,3 +3,4 @@ fuzz/
cmd/tomll/tomll cmd/tomll/tomll
cmd/tomljson/tomljson cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen cmd/tomltestgen/tomltestgen
dist
+1 -1
View File
@@ -60,7 +60,7 @@ enable = [
# "nlreturn", # "nlreturn",
"noctx", "noctx",
"nolintlint", "nolintlint",
"paralleltest", #"paralleltest",
"prealloc", "prealloc",
"predeclared", "predeclared",
"revive", "revive",
+123
View File
@@ -0,0 +1,123 @@
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
- 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
- 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_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
+23 -9
View File
@@ -155,6 +155,8 @@ Checklist:
- Does not introduce backward-incompatible changes (unless discussed). - Does not introduce backward-incompatible changes (unless discussed).
- Has relevant doc changes. - Has relevant doc changes.
- Benchstat does not show performance regression. - Benchstat does not show performance regression.
- Pull request is [labeled appropriately][pr-labels].
- Title will be understandable in the changelog.
1. Merge using "squash and merge". 1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information 2. Make sure to edit the commit message to keep all the useful information
@@ -163,13 +165,25 @@ Checklist:
### New release ### New release
1. Go to [releases][releases]. Click on "X commits to master since this 1. Decide on the next version number. Use semver.
release". 2. Generate release notes using [`gh`][gh]. Example:
2. Make note of all the changes. Look for backward incompatible changes, ```
new features, and bug fixes. $ gh api -X POST \
3. Pick the new version using the above and semver. -F tag_name='v2.0.0-beta.5' \
4. Create a [new release][new-release]. -F target_commitish='v2' \
5. Follow the same format as [1.1.0][release-110]. -F previous_tag_name='v2.0.0-beta.4' \
--jq '.body' \
repos/pelletier/go-toml/releases/generate-notes
```
3. Look for "Other changes". That would indicate a pull request not labeled
properly. Tweak labels and pull request titles until changelog looks good for
users.
4. [Draft new release][new-release].
5. Fill tag and target with the same value used to generate the changelog.
6. Set title to the new tag value.
7. Paste the generated changelog.
8. Check "create discussion", in the "Releases" category.
9. Check pre-release if new version is an alpha or beta.
[issues-tracker]: https://github.com/pelletier/go-toml/issues [issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md [bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
@@ -177,6 +191,6 @@ Checklist:
[readme]: ./README.md [readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo [fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request [pull-request]: https://help.github.com/en/articles/creating-a-pull-request
[releases]: https://github.com/pelletier/go-toml/releases
[new-release]: https://github.com/pelletier/go-toml/releases/new [new-release]: https://github.com/pelletier/go-toml/releases/new
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0 [gh]: https://github.com/cli/cli
[pr-labels]: https://github.com/pelletier/go-toml/blob/v2/.github/release.yml
+5
View File
@@ -0,0 +1,5 @@
FROM scratch
ENV PATH "$PATH:/bin"
COPY tomll /bin/tomll
COPY tomljson /bin/tomljson
COPY jsontoml /bin/jsontoml
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton Copyright (c) 2013 - 2022 Thomas Pelletier, Eric Anderton
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+226 -26
View File
@@ -4,32 +4,25 @@ Go library for the [TOML](https://toml.io/en/) format.
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0). This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
## Development status [💬 Anything else](https://github.com/pelletier/go-toml/discussions)
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).
## Documentation ## Documentation
Full API, examples, and implementation notes are available in the Go documentation. Full API, examples, and implementation notes are available in the Go
documentation.
[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2) [![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
## Import ## Import
```go ```go
import "github.com/pelletier/go-toml/v2" import "github.com/pelletier/go-toml/v2"
``` ```
See [Modules](#Modules).
## Features ## Features
### Stdlib behavior ### Stdlib behavior
@@ -40,20 +33,21 @@ standard library's `encoding/json`.
### Performance ### Performance
While go-toml favors usability, it is written with performance in mind. Most While go-toml favors usability, it is written with performance in mind. Most
operations should not be shockingly slow. operations should not be shockingly slow. See [benchmarks](#benchmarks).
### Strict mode ### Strict mode
`Decoder` can be set to "strict mode", which makes it error when some parts of `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]. to check for typos. [See example in the documentation][strict].
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.SetStrict [strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.DisallowUnknownFields
### Contextualized errors ### Contextualized errors
When decoding errors occur, go-toml returns [`DecodeError`][decode-err]), which When most decoding errors occur, go-toml returns [`DecodeError`][decode-err]),
contains a human readable contextualized version of the error. For example: which contains a human readable contextualized version of the error. For
example:
``` ```
2| key1 = "value1" 2| key1 = "value1"
@@ -146,6 +140,101 @@ fmt.Println(string(b))
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal [marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
## 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.8x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.4x</td><td>5.3x</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
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.8x</td><td>2.9x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>4.2x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.5x</td><td>3.1x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.2x</td><td>3.9x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.1x</td><td>3.5x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>3.1x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.5x</td><td>2.6x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.1x</td><td>2.2x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.6x</td><td>1.3x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.3x</td><td>3.2x</td></tr>
<tr><td>[Geo mean]</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>
## Modules
go-toml uses Go's standard modules system.
Installation instructions:
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
with it automatically.
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
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 availble on [ghcr.io][docker].
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
## Migrating from v1 ## Migrating from v1
This section describes the differences between v1 and v2, with some pointers on This section describes the differences between v1 and v2, with some pointers on
@@ -187,7 +276,7 @@ d := doc{
} }
data := ` data := `
[A] [A]
B = "After" B = "After"
` `
@@ -248,6 +337,45 @@ This method was not widely used, poorly defined, and added a lot of complexity.
A similar effect can be achieved by implementing the `encoding.TextUnmarshaler` A similar effect can be achieved by implementing the `encoding.TextUnmarshaler`
interface and use strings. interface and use strings.
#### Support for `default` struct tag has been dropped
This feature adds complexity and a poorly defined API for an effect that can be
accomplished outside of the library.
It does not seem like other format parsers in Go support that feature (the
project referenced in the original ticket #202 has not been updated since 2017).
Given that go-toml v2 should not touch values not in the document, the same
effect can be achieved by pre-filling the struct with defaults (libraries like
[go-defaults][go-defaults] can help). Also, string representation is not well
defined for all types: it creates issues like #278.
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 ### Encoding / Marshal
#### Default struct fields order #### Default struct fields order
@@ -283,14 +411,14 @@ fmt.Println("v2:\n" + string(b))
``` ```
There is no way to make v2 encoder behave like v1. A workaround could be to 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 #### No indentation by default
V1 automatically indents content of tables by default. V2 does not. However the V1 automatically indents content of tables by default. V2 does not. However the
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example: same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
```go ```go
data := map[string]interface{}{ data := map[string]interface{}{
"table": map[string]string{ "table": map[string]string{
@@ -312,15 +440,15 @@ fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
// Output: // Output:
// v1: // v1:
// //
// [table] // [table]
// key = "value" // key = "value"
// //
// v2: // v2:
// [table] // [table]
// key = 'value' // key = 'value'
// //
// //
// v2 Encoder: // v2 Encoder:
// [table] // [table]
// key = 'value' // key = 'value'
@@ -332,7 +460,9 @@ fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
V1 always uses double quotes (`"`) around strings and keys that cannot be V1 always uses double quotes (`"`) around strings and keys that cannot be
represented bare (unquoted). V2 uses single quotes instead by default (`'`), 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. There is no way to make v2 encoder behave like v1.
@@ -347,6 +477,76 @@ There is no way to make v2 encoder behave like v1.
[tm]: https://golang.org/pkg/encoding/#TextMarshaler [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`, and `omitempty`. For example:
```go
type doc struct {
// v1
F string `toml:"field" multiline:"true" omitempty:"true"`
// v2
F string `toml:"field,multiline,omitempty"`
}
```
Has a result, the `Encoder.SetTag*` methods have been removed, as there is just
one tag now.
#### `commented` tag has been removed
There is no replacement for the `commented` tag. This feature would be better
suited in a proper document model for go-toml v2, which has been [cut from
scope][nodoc] at the moment.
#### `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
Go-toml follows [Semantic Versioning](http://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 ## License
The MIT License (MIT). Read [LICENSE](LICENSE). The MIT License (MIT). Read [LICENSE](LICENSE).
+19
View File
@@ -0,0 +1,19 @@
# Security Policy
## 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: |
| All 1.x | :x: |
| All 0.x | :x: |
## Reporting a Vulnerability
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
as many details as possible to reproduce the vulnerability. This is a
side-project: I will try to get back to you as quickly as possible, time
permitting in my personal life. Providing a working patch helps very much!
+16 -17
View File
@@ -31,13 +31,14 @@ var bench_inputs = []struct {
func TestUnmarshalDatasetCode(t *testing.T) { func TestUnmarshalDatasetCode(t *testing.T) {
for _, tc := range bench_inputs { for _, tc := range bench_inputs {
buf := fixture(t, tc.name)
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
buf := fixture(t, tc.name)
var v interface{} var v interface{}
check(t, toml.Unmarshal(buf, &v)) require.NoError(t, toml.Unmarshal(buf, &v))
b, err := json.Marshal(v) b, err := json.Marshal(v)
check(t, err) require.NoError(t, err)
require.Equal(t, len(b), tc.jsonLen) require.Equal(t, len(b), tc.jsonLen)
}) })
} }
@@ -45,14 +46,14 @@ func TestUnmarshalDatasetCode(t *testing.T) {
func BenchmarkUnmarshalDataset(b *testing.B) { func BenchmarkUnmarshalDataset(b *testing.B) {
for _, tc := range bench_inputs { for _, tc := range bench_inputs {
buf := fixture(b, tc.name)
b.Run(tc.name, func(b *testing.B) { b.Run(tc.name, func(b *testing.B) {
buf := fixture(b, tc.name)
b.SetBytes(int64(len(buf))) b.SetBytes(int64(len(buf)))
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var v interface{} var v interface{}
check(b, toml.Unmarshal(buf, &v)) require.NoError(b, toml.Unmarshal(buf, &v))
} }
}) })
} }
@@ -60,22 +61,20 @@ func BenchmarkUnmarshalDataset(b *testing.B) {
// fixture returns the uncompressed contents of path. // fixture returns the uncompressed contents of path.
func fixture(tb testing.TB, path string) []byte { func fixture(tb testing.TB, path string) []byte {
f, err := os.Open(filepath.Join("testdata", path+".toml.gz")) tb.Helper()
check(tb, err)
file := path + ".toml.gz"
f, err := os.Open(filepath.Join("testdata", file))
if os.IsNotExist(err) {
tb.Skip("benchmark fixture not found:", file)
}
require.NoError(tb, err)
defer f.Close() defer f.Close()
gz, err := gzip.NewReader(f) gz, err := gzip.NewReader(f)
check(tb, err) require.NoError(tb, err)
buf, err := ioutil.ReadAll(gz) buf, err := ioutil.ReadAll(gz)
check(tb, err) require.NoError(tb, err)
return buf return buf
} }
func check(tb testing.TB, err error) {
if err != nil {
tb.Helper()
tb.Fatal(err)
}
}
+1 -1
View File
@@ -186,7 +186,7 @@ key3 = 1979-05-27T00:32:00.999999-07:00
key1 = [ 1, 2, 3 ] key1 = [ 1, 2, 3 ]
key2 = [ "red", "yellow", "green" ] key2 = [ "red", "yellow", "green" ]
key3 = [ [ 1, 2 ], [3, 4, 5] ] key3 = [ [ 1, 2 ], [3, 4, 5] ]
#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays # Arrays can also be multiline. So in addition to ignoring whitespace, arrays
# also ignore newlines between the brackets. Terminating commas are ok before # also ignore newlines between the brackets. Terminating commas are ok before
+529 -29
View File
@@ -1,6 +1,7 @@
package benchmark_test package benchmark_test
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"testing" "testing"
"time" "time"
@@ -9,17 +10,230 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func BenchmarkUnmarshalSimple(b *testing.B) { func TestUnmarshalSimple(t *testing.T) {
doc := []byte(`A = "hello"`)
d := struct { d := struct {
A string A string
}{} }{}
doc := []byte(`A = "hello"`)
for i := 0; i < b.N; i++ { err := toml.Unmarshal(doc, &d)
err := toml.Unmarshal(doc, &d) if err != nil {
panic(err)
}
}
func BenchmarkUnmarshal(b *testing.B) {
b.Run("SimpleDocument", func(b *testing.B) {
doc := []byte(`A = "hello"`)
b.Run("struct", func(b *testing.B) {
b.SetBytes(int64(len(doc)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := struct {
A string
}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
})
b.Run("map", func(b *testing.B) {
b.SetBytes(int64(len(doc)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := map[string]interface{}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
})
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.Run("struct", func(b *testing.B) {
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := benchmarkDoc{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
b.Run("map", func(b *testing.B) {
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := map[string]interface{}{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
})
b.Run("HugoFrontMatter", func(b *testing.B) {
b.SetBytes(int64(len(hugoFrontMatterbytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := map[string]interface{}{}
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
if err != nil {
panic(err)
}
}
})
}
func marshal(v interface{}) ([]byte, error) {
var b bytes.Buffer
enc := toml.NewEncoder(&b)
err := enc.Encode(v)
return b.Bytes(), err
}
func BenchmarkMarshal(b *testing.B) {
b.Run("SimpleDocument", func(b *testing.B) {
doc := []byte(`A = "hello"`)
b.Run("struct", func(b *testing.B) {
d := struct {
A string
}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
b.Run("map", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.Run("struct", func(b *testing.B) {
d := benchmarkDoc{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
b.Run("map", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
})
b.Run("HugoFrontMatter", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
if err != nil { if err != nil {
panic(err) panic(err)
} }
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
} }
type benchmarkDoc struct { type benchmarkDoc struct {
@@ -35,7 +249,7 @@ type benchmarkDoc struct {
} }
Point struct { Point struct {
X int64 X int64
U int64 Y int64
} }
} }
} }
@@ -108,6 +322,7 @@ type benchmarkDoc struct {
Key2 []string Key2 []string
Key3 [][]int64 Key3 [][]int64
// TODO: Key4 not supported by go-toml's Unmarshal // TODO: Key4 not supported by go-toml's Unmarshal
Key4 []interface{}
Key5 []int64 Key5 []int64
Key6 []int64 Key6 []int64
} }
@@ -119,36 +334,321 @@ type benchmarkDoc struct {
Fruit []struct { Fruit []struct {
Name string Name string
Physical struct { Physical struct {
Color string Color string
Shape string Shape string
Variety []struct { }
Name string Variety []struct {
} Name string
} }
} }
} }
func BenchmarkReferenceFile(b *testing.B) { func TestUnmarshalReferenceFile(t *testing.T) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := benchmarkDoc{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
}
func TestReferenceFile(t *testing.T) {
bytes, err := ioutil.ReadFile("benchmark.toml") bytes, err := ioutil.ReadFile("benchmark.toml")
require.NoError(t, err) require.NoError(t, err)
d := benchmarkDoc{} d := benchmarkDoc{}
err = toml.Unmarshal(bytes, &d) err = toml.Unmarshal(bytes, &d)
require.NoError(t, err) require.NoError(t, err)
expected := benchmarkDoc{
Table: struct {
Key string
Subtable struct{ Key string }
Inline struct {
Name struct {
First string
Last string
}
Point struct {
X int64
Y int64
}
}
}{
Key: "value",
Subtable: struct{ Key string }{
Key: "another value",
},
// note: x.y.z.w is purposefully missing
Inline: struct {
Name struct {
First string
Last string
}
Point struct {
X int64
Y int64
}
}{
Name: struct {
First string
Last string
}{
First: "Tom",
Last: "Preston-Werner",
},
Point: struct {
X int64
Y int64
}{
X: 1,
Y: 2,
},
},
},
String: struct {
Basic struct{ Basic string }
Multiline struct {
Key1 string
Key2 string
Key3 string
Continued struct {
Key1 string
Key2 string
Key3 string
}
}
Literal struct {
Winpath string
Winpath2 string
Quoted string
Regex string
Multiline struct {
Regex2 string
Lines string
}
}
}{
Basic: struct{ Basic string }{
Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.",
},
Multiline: struct {
Key1 string
Key2 string
Key3 string
Continued struct {
Key1 string
Key2 string
Key3 string
}
}{
Key1: "One\nTwo",
Key2: "One\nTwo",
Key3: "One\nTwo",
Continued: struct {
Key1 string
Key2 string
Key3 string
}{
Key1: `The quick brown fox jumps over the lazy dog.`,
Key2: `The quick brown fox jumps over the lazy dog.`,
Key3: `The quick brown fox jumps over the lazy dog.`,
},
},
Literal: struct {
Winpath string
Winpath2 string
Quoted string
Regex string
Multiline struct {
Regex2 string
Lines string
}
}{
Winpath: `C:\Users\nodejs\templates`,
Winpath2: `\\ServerX\admin$\system32\`,
Quoted: `Tom "Dubs" Preston-Werner`,
Regex: `<\i\c*\s*>`,
Multiline: struct {
Regex2 string
Lines string
}{
Regex2: `I [dw]on't need \d{2} apples`,
Lines: `The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
`,
},
},
},
Integer: struct {
Key1 int64
Key2 int64
Key3 int64
Key4 int64
Underscores struct {
Key1 int64
Key2 int64
Key3 int64
}
}{
Key1: 99,
Key2: 42,
Key3: 0,
Key4: -17,
Underscores: struct {
Key1 int64
Key2 int64
Key3 int64
}{
Key1: 1000,
Key2: 5349221,
Key3: 12345,
},
},
Float: struct {
Fractional struct {
Key1 float64
Key2 float64
Key3 float64
}
Exponent struct {
Key1 float64
Key2 float64
Key3 float64
}
Both struct{ Key float64 }
Underscores struct {
Key1 float64
Key2 float64
}
}{
Fractional: struct {
Key1 float64
Key2 float64
Key3 float64
}{
Key1: 1.0,
Key2: 3.1415,
Key3: -0.01,
},
Exponent: struct {
Key1 float64
Key2 float64
Key3 float64
}{
Key1: 5e+22,
Key2: 1e6,
Key3: -2e-2,
},
Both: struct{ Key float64 }{
Key: 6.626e-34,
},
Underscores: struct {
Key1 float64
Key2 float64
}{
Key1: 9224617.445991228313,
Key2: 1e100,
},
},
Boolean: struct {
True bool
False bool
}{
True: true,
False: false,
},
Datetime: struct {
Key1 time.Time
Key2 time.Time
Key3 time.Time
}{
Key1: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Key2: time.Date(1979, 5, 27, 0, 32, 0, 0, time.FixedZone("", -7*3600)),
Key3: time.Date(1979, 5, 27, 0, 32, 0, 999999000, time.FixedZone("", -7*3600)),
},
Array: struct {
Key1 []int64
Key2 []string
Key3 [][]int64
Key4 []interface{}
Key5 []int64
Key6 []int64
}{
Key1: []int64{1, 2, 3},
Key2: []string{"red", "yellow", "green"},
Key3: [][]int64{{1, 2}, {3, 4, 5}},
Key4: []interface{}{
[]interface{}{int64(1), int64(2)},
[]interface{}{"a", "b", "c"},
},
Key5: []int64{1, 2, 3},
Key6: []int64{1, 2},
},
Products: []struct {
Name string
Sku int64
Color string
}{
{
Name: "Hammer",
Sku: 738594937,
},
{},
{
Name: "Nail",
Sku: 284758393,
Color: "gray",
},
},
Fruit: []struct {
Name string
Physical struct {
Color string
Shape string
}
Variety []struct{ Name string }
}{
{
Name: "apple",
Physical: struct {
Color string
Shape string
}{
Color: "red",
Shape: "round",
},
Variety: []struct{ Name string }{
{Name: "red delicious"},
{Name: "granny smith"},
},
},
{
Name: "banana",
Variety: []struct{ Name string }{
{Name: "plantain"},
},
},
},
}
require.Equal(t, expected, d)
} }
var hugoFrontMatterbytes = []byte(`
categories = ["Development", "VIM"]
date = "2012-04-06"
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
slug = "spf13-vim-3-0-release-and-new-website"
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
title = "spf13-vim 3.0 release and new website"
include_toc = true
show_comments = false
[[cascade]]
background = "yosemite.jpg"
[cascade._target]
kind = "page"
lang = "en"
path = "/blog/**"
[[cascade]]
background = "goldenbridge.jpg"
[cascade._target]
kind = "section"
`)
+71
View File
@@ -0,0 +1,71 @@
package toml
import (
"bytes"
"testing"
)
var valid10Ascii = []byte("1234567890")
var valid10Utf8 = []byte("日本語a")
var valid1kUtf8 = bytes.Repeat([]byte("0123456789日本語日本語日本語日abcdefghijklmnopqrstuvwx"), 16)
var valid1MUtf8 = bytes.Repeat(valid1kUtf8, 1024)
var valid1kAscii = bytes.Repeat([]byte("012345678998jhjklasDJKLAAdjdfjsdklfjdslkabcdefghijklmnopqrstuvwx"), 16)
var valid1MAscii = bytes.Repeat(valid1kAscii, 1024)
func BenchmarkScanComments(b *testing.B) {
wrap := func(x []byte) []byte {
return []byte("# " + string(x) + "\n")
}
inputs := map[string][]byte{
"10Valid": wrap(valid10Ascii),
"1kValid": wrap(valid1kAscii),
"1MValid": wrap(valid1MAscii),
"10ValidUtf8": wrap(valid10Utf8),
"1kValidUtf8": wrap(valid1kUtf8),
"1MValidUtf8": wrap(valid1MUtf8),
}
for name, input := range inputs {
b.Run(name, func(b *testing.B) {
b.SetBytes(int64(len(input)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
scanComment(input)
}
})
}
}
func BenchmarkParseLiteralStringValid(b *testing.B) {
wrap := func(x []byte) []byte {
return []byte("'" + string(x) + "'")
}
inputs := map[string][]byte{
"10Valid": wrap(valid10Ascii),
"1kValid": wrap(valid1kAscii),
"1MValid": wrap(valid1MAscii),
"10ValidUtf8": wrap(valid10Utf8),
"1kValidUtf8": wrap(valid1kUtf8),
"1MValidUtf8": wrap(valid1MUtf8),
}
for name, input := range inputs {
b.Run(name, func(b *testing.B) {
p := parser{}
b.SetBytes(int64(len(input)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, err := p.parseLiteralString(input)
if err != nil {
panic(err)
}
}
})
}
}
+125 -8
View File
@@ -39,6 +39,12 @@ benchmark [OPTIONS...] [BRANCH]
-d Compare benchmarks of HEAD with BRANCH using benchstats. In -d Compare benchmarks of HEAD with BRANCH using benchstats. In
this form the BRANCH argument is required. this form the BRANCH argument is required.
-a Compare benchmarks of HEAD against go-toml v1 and
BurntSushi/toml.
-html When used with -a, emits the output as HTML, ready to be
embedded in the README.
coverage [OPTIONS...] [BRANCH] coverage [OPTIONS...] [BRANCH]
Generates code coverage. Generates code coverage.
@@ -70,7 +76,8 @@ cover() {
fi fi
pushd "$dir" pushd "$dir"
go test -covermode=atomic -coverprofile=coverage.out ./... go test -covermode=atomic -coverpkg=./... -coverprofile=coverage.out.tmp ./...
cat coverage.out.tmp | grep -v testsuite | grep -v tomltestgen | grep -v gotoml-test-decoder > coverage.out
go tool cover -func=coverage.out go tool cover -func=coverage.out
popd popd
@@ -97,16 +104,23 @@ coverage() {
echo "" echo ""
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')" target_pct="$(tail -n2 ${target_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%.*/\1/')"
head_pct="$(cat ${head_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')" head_pct="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')"
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%" echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
delta_pct=$(echo "$head_pct - $target_pct" | bc -l) delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
echo "Delta: ${delta_pct}" echo "Delta: ${delta_pct}"
if [[ $delta_pct = \-* ]]; then if [[ $delta_pct = \-* ]]; then
echo "Regression!"; echo "Regression!";
return 1
target_diff="${output_dir}/target.diff.txt"
head_diff="${output_dir}/head.diff.txt"
cat "${target_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${target_diff}"
cat "${head_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${head_diff}"
diff --side-by-side --suppress-common-lines "${target_diff}" "${head_diff}"
return 1
fi fi
return 0 return 0
;; ;;
@@ -118,6 +132,7 @@ coverage() {
bench() { bench() {
branch="${1}" branch="${1}"
out="${2}" out="${2}"
replace="${3}"
dir="$(mktemp -d)" dir="$(mktemp -d)"
stderr "Executing benchmark for ${branch} at ${dir}" stderr "Executing benchmark for ${branch} at ${dir}"
@@ -129,7 +144,14 @@ bench() {
fi fi
pushd "$dir" pushd "$dir"
go test -bench=. -count=10 ./... | tee "${out}"
if [ "${replace}" != "" ]; then
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
go get "${replace}"
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}"
popd popd
if [ "${branch}" != "HEAD" ]; then if [ "${branch}" != "HEAD" ]; then
@@ -137,19 +159,114 @@ bench() {
fi fi
} }
fmktemp() {
if mktemp --version|grep GNU >/dev/null; then
mktemp --suffix=-$1;
else
mktemp -t $1;
fi
}
benchstathtml() {
python3 - $1 <<'EOF'
import sys
lines = []
stop = False
with open(sys.argv[1]) as f:
for line in f.readlines():
line = line.strip()
if line == "":
stop = True
if not stop:
lines.append(line.split(','))
results = []
for line in reversed(lines[1:]):
v2 = float(line[1])
results.append([
line[0].replace("-32", ""),
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[5])/v2), # bs
])
# move geomean to the end
results.append(results[0])
del results[0]
def printtable(data):
print("""
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>""")
for r in data:
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
print(""" </tbody>
</table>""")
def match(x):
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
above = [x for x in results if match(x)]
below = [x for x in results if not match(x)]
printtable(above)
print("<details><summary>See more</summary>")
print("""<p>The table above has the results of the most common use-cases. The table below
contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>""")
printtable(below)
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
print("</details>")
EOF
}
benchmark() { benchmark() {
case "$1" in case "$1" in
-d) -d)
shift shift
target="${1?Need to provide a target branch argument}" target="${1?Need to provide a target branch argument}"
old=`mktemp`
old=`fmktemp ${target}`
bench "${target}" "${old}" bench "${target}" "${old}"
new=`mktemp` new=`fmktemp HEAD`
bench HEAD "${new}" bench HEAD "${new}"
benchstat "${old}" "${new}" benchstat "${old}" "${new}"
return 0 return 0
;; ;;
-a)
shift
v2stats=`fmktemp go-toml-v2`
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
v1stats=`fmktemp go-toml-v1`
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
bsstats=`fmktemp bs-toml`
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
cp "${v2stats}" go-toml-v2.txt
cp "${v1stats}" go-toml-v1.txt
cp "${bsstats}" bs-toml.txt
if [ "$1" = "-html" ]; then
tmpcsv=`fmktemp csv`
benchstat -csv -geomean 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
fi
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
return $?
esac esac
bench "${1-HEAD}" `mktemp` bench "${1-HEAD}" `mktemp`
+30
View File
@@ -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.DecodeStdin()
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)
}
+55
View File
@@ -0,0 +1,55 @@
// Package jsontoml is a program that converts JSON to TOML.
//
// # Usage
//
// Reading from stdin:
//
// cat file.json | jsontoml > file.toml
//
// Reading from a file:
//
// jsontoml file.json > file.toml
//
// # Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
package main
import (
"encoding/json"
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `jsontoml can be used in two ways:
Reading from stdin:
cat file.json | jsontoml > file.toml
Reading from a file:
jsontoml file.json > file.toml
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := json.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
return err
}
e := toml.NewEncoder(w)
return e.Encode(v)
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvert(t *testing.T) {
examples := []struct {
name string
input string
expected string
errors bool
}{
{
name: "valid json",
input: `
{
"mytoml": {
"a": 42
}
}`,
expected: `[mytoml]
a = 42.0
`,
},
{
name: "invalid json",
input: `{ foo`,
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
err := convert(strings.NewReader(e.input), b)
if e.errors {
require.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, e.expected, b.String())
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Package tomljson is a program that converts TOML to JSON.
//
// # Usage
//
// Reading from stdin:
//
// cat file.toml | tomljson > file.json
//
// Reading from a file:
//
// tomljson file.toml > file.json
//
// # Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `tomljson can be used in two ways:
Reading from stdin:
cat file.toml | tomljson > file.json
Reading from a file:
tomljson file.toml > file.json
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := toml.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
var derr *toml.DecodeError
if errors.As(err, &derr) {
row, col := derr.Position()
return fmt.Errorf("%s\nerror occurred at row %d column %d", derr.String(), row, col)
}
return err
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
return e.Encode(v)
}
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvert(t *testing.T) {
examples := []struct {
name string
input io.Reader
expected string
errors bool
}{
{
name: "valid toml",
input: strings.NewReader(`
[mytoml]
a = 42`),
expected: `{
"mytoml": {
"a": 42
}
}
`,
},
{
name: "invalid toml",
input: strings.NewReader(`bad = []]`),
errors: true,
},
{
name: "bad reader",
input: &badReader{},
errors: true,
},
}
for _, e := range examples {
b := new(bytes.Buffer)
err := convert(e.input, b)
if e.errors {
require.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, e.expected, b.String())
}
}
}
type badReader struct{}
func (r *badReader) Read([]byte) (int, error) {
return 0, fmt.Errorf("reader failed on purpose")
}
+58
View File
@@ -0,0 +1,58 @@
// Package tomll is a linter program for TOML.
//
// # Usage
//
// Reading from stdin, writing to stdout:
//
// cat file.toml | tomll
//
// Reading and updating a list of files in place:
//
// tomll a.toml b.toml c.toml
//
// # Installation
//
// Using Go:
//
// go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
package main
import (
"io"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/cli"
)
const usage = `tomll can be used in two ways:
Reading from stdin, writing to stdout:
cat file.toml | tomll > file.toml
Reading and updating a list of files in place:
tomll a.toml b.toml c.toml
When given a list of files, tomll will modify all files in place without asking.
`
func main() {
p := cli.Program{
Usage: usage,
Fn: convert,
Inplace: true,
}
p.Execute()
}
func convert(r io.Reader, w io.Writer) error {
var v interface{}
d := toml.NewDecoder(r)
err := d.Decode(&v)
if err != nil {
return err
}
e := toml.NewEncoder(w)
return e.Encode(v)
}
+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())
}
}
}
+223
View File
@@ -0,0 +1,223 @@
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
//
// Within the go-toml package, run `go generate`. Otherwise, use:
//
// 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"
"strconv"
"strings"
"text/template"
"time"
)
type invalid struct {
Name string
Input string
}
type valid struct {
Name string
Input string
JsonRef string
}
type testsCollection struct {
Ref string
Timestamp string
Invalid []invalid
Valid []valid
Count int
}
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
"package toml_test\n" +
" import (\n" +
" \"testing\"\n" +
")\n" +
"{{range .Invalid}}\n" +
"func TestTOMLTest_Invalid_{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" testgenInvalid(t, input)\n" +
"}\n" +
"{{end}}\n" +
"\n" +
"{{range .Valid}}\n" +
"func TestTOMLTest_Valid_{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" jsonRef := {{.JsonRef|gostr}}\n" +
" testgenValid(t, input, jsonRef)\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
for _, c := range kebab {
if nextUpper {
camel += strings.ToUpper(string(c))
nextUpper = false
} else if c == '-' {
nextUpper = true
} else if c == '/' {
nextUpper = true
camel += "_"
} else {
camel += string(c)
}
}
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)
}
var (
ref = flag.String("r", "master", "git reference")
out = flag.String("o", "", "output file")
)
func usage() {
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
flag.PrintDefaults()
}
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{}
for _, f := range zipReader.File {
zipFilesMap[f.Name] = f
}
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]
log.Printf("> [%s] %s\n", testType, 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))
}
}
}
log.Printf("Collected %d tests from toml-test\n", collection.Count)
funcMap := template.FuncMap{
"gostr": templateGoStr,
}
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
buf := new(bytes.Buffer)
err = t.Execute(buf, collection)
if err != nil {
panic(err)
}
outputBytes, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
if *out == "" {
fmt.Println(string(outputBytes))
return
}
err = os.WriteFile(*out, outputBytes, 0644)
if err != nil {
panic(err)
}
}
+273 -43
View File
@@ -35,26 +35,42 @@ func parseLocalDate(b []byte) (LocalDate, error) {
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD") return date, newDecodeError(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 = time.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 date, nil return date, nil
} }
func parseDecimalDigits(b []byte) int { func parseDecimalDigits(b []byte) (int, error) {
v := 0 v := 0
for _, c := range b { for i, c := range b {
if c < '0' || c > '9' {
return 0, newDecodeError(b[i:i+1], "expected digit (0-9)")
}
v *= 10 v *= 10
v += int(c - '0') v += int(c - '0')
} }
return v return v, nil
} }
func parseDateTime(b []byte) (time.Time, error) { func parseDateTime(b []byte) (time.Time, error) {
@@ -75,7 +91,7 @@ func parseDateTime(b []byte) (time.Time, error) {
panic("date time should have a timezone") panic("date time should have a timezone")
} }
if b[0] == 'Z' { if b[0] == 'Z' || b[0] == 'z' {
b = b[1:] b = b[1:]
zone = time.UTC zone = time.UTC
} else { } else {
@@ -83,15 +99,42 @@ func parseDateTime(b []byte) (time.Time, error) {
if len(b) != dateTimeByteLen { if len(b) != dateTimeByteLen {
return time.Time{}, newDecodeError(b, "invalid date-time timezone") return time.Time{}, newDecodeError(b, "invalid date-time timezone")
} }
direction := 1 var direction int
if b[0] == '-' { switch b[0] {
case '-':
direction = -1 direction = -1
case '+':
direction = +1
default:
return time.Time{}, newDecodeError(b[:1], "invalid timezone offset character")
}
if b[3] != ':' {
return time.Time{}, newDecodeError(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{}, newDecodeError(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{}, newDecodeError(b[:1], "invalid timezone offset minutes")
} }
hours := digitsToInt(b[1:3])
minutes := digitsToInt(b[4:6])
seconds := direction * (hours*3600 + minutes*60) seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds) if seconds == 0 {
zone = time.UTC
} else {
zone = time.FixedZone("", seconds)
}
b = b[dateTimeByteLen:] b = b[dateTimeByteLen:]
} }
@@ -100,13 +143,13 @@ func parseDateTime(b []byte) (time.Time, error) {
} }
t := time.Date( t := time.Date(
dt.Date.Year, dt.Year,
dt.Date.Month, time.Month(dt.Month),
dt.Date.Day, dt.Day,
dt.Time.Hour, dt.Hour,
dt.Time.Minute, dt.Minute,
dt.Time.Second, dt.Second,
dt.Time.Nanosecond, dt.Nanosecond,
zone) zone)
return t, nil return t, nil
@@ -124,10 +167,10 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
if err != nil { if err != nil {
return dt, nil, err return dt, nil, err
} }
dt.Date = date dt.LocalDate = date
sep := b[10] sep := b[10]
if sep != 'T' && sep != ' ' { 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, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
} }
@@ -135,7 +178,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
if err != nil { if err != nil {
return dt, nil, err return dt, nil, err
} }
dt.Time = t dt.LocalTime = t
return dt, rest, nil return dt, rest, nil
} }
@@ -149,45 +192,89 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
t LocalTime t LocalTime
) )
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
const localTimeByteLen = 8 const localTimeByteLen = 8
if len(b) < localTimeByteLen { if len(b) < localTimeByteLen {
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]") return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
} }
t.Hour = parseDecimalDigits(b[0:2]) var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
}
if t.Hour > 23 {
return t, nil, newDecodeError(b[0:2], "hour cannot be greater 23")
}
if b[2] != ':' { if b[2] != ':' {
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes") return t, nil, newDecodeError(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")
}
if b[5] != ':' { if b[5] != ':' {
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds") return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
} }
t.Second = parseDecimalDigits(b[6:8]) t.Second, err = parseDecimalDigits(b[6:8])
if err != nil {
return t, nil, err
}
const minLengthWithFrac = 9 if t.Second > 60 {
if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' { return t, nil, newDecodeError(b[6:8], "seconds cannot be greater 60")
}
b = b[8:]
if len(b) >= 1 && b[0] == '.' {
frac := 0 frac := 0
precision := 0
digits := 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[0:1], "need at least one digit after fraction point")
}
break
}
digits++
const maxFracPrecision = 9 const maxFracPrecision = 9
if i >= maxFracPrecision { 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 *= 10
frac += int(c - '0') frac += int(c - '0')
digits++ precision++
} }
t.Nanosecond = frac * nspow[digits] if precision == 0 {
return t, nil, newDecodeError(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, nil
return t, b[8:], nil
} }
//nolint:cyclop //nolint:cyclop
@@ -196,7 +283,7 @@ func parseFloat(b []byte) (float64, error) {
return math.NaN(), nil return math.NaN(), nil
} }
cleaned, err := checkAndRemoveUnderscores(b) cleaned, err := checkAndRemoveUnderscoresFloats(b)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -209,6 +296,30 @@ func parseFloat(b []byte) (float64, error) {
return 0, newDecodeError(b, "float cannot end with a dot") return 0, newDecodeError(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")
}
if !isDigit(cleaned[i-1]) {
return 0, newDecodeError(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")
}
dotAlreadySeen = true
}
}
start := 0
if cleaned[0] == '+' || cleaned[0] == '-' {
start = 1
}
if cleaned[start] == '0' && isDigit(cleaned[start+1]) {
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
}
f, err := strconv.ParseFloat(string(cleaned), 64) f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil { if err != nil {
return 0, newDecodeError(b, "unable to parse float: %w", err) return 0, newDecodeError(b, "unable to parse float: %w", err)
@@ -218,7 +329,7 @@ func parseFloat(b []byte) (float64, error) {
} }
func parseIntHex(b []byte) (int64, error) { func parseIntHex(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:]) cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -232,7 +343,7 @@ func parseIntHex(b []byte) (int64, error) {
} }
func parseIntOct(b []byte) (int64, error) { func parseIntOct(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:]) cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -246,7 +357,7 @@ func parseIntOct(b []byte) (int64, error) {
} }
func parseIntBin(b []byte) (int64, error) { func parseIntBin(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:]) cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -259,12 +370,26 @@ func parseIntBin(b []byte) (int64, error) {
return i, nil return i, nil
} }
func isSign(b byte) bool {
return b == '+' || b == '-'
}
func parseIntDec(b []byte) (int64, error) { func parseIntDec(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b) cleaned, err := checkAndRemoveUnderscoresIntegers(b)
if err != nil { if err != nil {
return 0, err return 0, err
} }
startIdx := 0
if isSign(cleaned[0]) {
startIdx++
}
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
return 0, newDecodeError(b, "leading zero not allowed on decimal number")
}
i, err := strconv.ParseInt(string(cleaned), 10, 64) i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil { if err != nil {
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err) return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
@@ -273,9 +398,18 @@ func parseIntDec(b []byte) (int64, error) {
return i, nil return i, nil
} }
func checkAndRemoveUnderscores(b []byte) ([]byte, error) { func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
if b[0] == '_' { start := 0
return nil, newDecodeError(b[0:1], "number cannot start with underscore") if b[start] == '+' || b[start] == '-' {
start++
}
if len(b) == start {
return b, nil
}
if b[start] == '_' {
return nil, newDecodeError(b[start:start+1], "number cannot start with underscore")
} }
if b[len(b)-1] == '_' { if b[len(b)-1] == '_' {
@@ -312,3 +446,99 @@ func checkAndRemoveUnderscores(b []byte) ([]byte, error) {
return cleaned, nil return cleaned, nil
} }
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
if b[0] == '_' {
return nil, newDecodeError(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")
}
// fast path
i := 0
for ; i < len(b); i++ {
if b[i] == '_' {
break
}
}
if i == len(b) {
return b, nil
}
before := false
cleaned := make([]byte, 0, len(b))
for i := 0; i < len(b); i++ {
c := b[i]
switch c {
case '_':
if !before {
return nil, newDecodeError(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, newDecodeError(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")
}
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")
}
if i > 0 && b[i-1] == '_' {
return nil, newDecodeError(b[i-1:i], "cannot have underscore before decimal point")
}
cleaned = append(cleaned, c)
default:
before = true
cleaned = append(cleaned, c)
}
}
return cleaned, nil
}
// isValidDate checks if a provided date is a date that exists.
func isValidDate(year int, month int, day int) bool {
return month > 0 && month < 13 && day > 0 && day <= daysIn(month, year)
}
// daysBefore[m] counts the number of days in a non-leap year
// before month m begins. There is an entry for m=12, counting
// the number of days before January of next year (365).
var daysBefore = [...]int32{
0,
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
}
func daysIn(m int, year int) int {
if m == 2 && isLeap(year) {
return 29
}
return int(daysBefore[m] - daysBefore[m-1])
}
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
+14 -4
View File
@@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/pelletier/go-toml/v2/internal/unsafe" "github.com/pelletier/go-toml/v2/internal/danger"
) )
// DecodeError represents an error encountered during the parsing or decoding // DecodeError represents an error encountered during the parsing or decoding
@@ -27,7 +27,7 @@ type DecodeError struct {
// corresponding field in the target value. It contains all the missing fields // corresponding field in the target value. It contains all the missing fields
// in Errors. // in Errors.
// //
// Emitted by Decoder when SetStrict(true) was called. // Emitted by Decoder when DisallowUnknownFields() was called.
type StrictMissingError struct { type StrictMissingError struct {
// One error per field that could not be found. // One error per field that could not be found.
Errors []DecodeError Errors []DecodeError
@@ -103,9 +103,10 @@ func (e *DecodeError) Key() Key {
// //
// The function copies all bytes used in DecodeError, so that document and // The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated. // highlight can be freely deallocated.
//
//nolint:funlen //nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) *DecodeError { func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
offset := unsafe.SubsliceOffset(document, de.highlight) offset := danger.SubsliceOffset(document, de.highlight)
errMessage := de.Error() errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset]) errLine, errColumn := positionAtEnd(document[:offset])
@@ -116,6 +117,7 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
maxLine := errLine + len(after) - 1 maxLine := errLine + len(after) - 1
lineColumnWidth := len(strconv.Itoa(maxLine)) lineColumnWidth := len(strconv.Itoa(maxLine))
// Write the lines of context strictly before the error.
for i := len(before) - 1; i > 0; i-- { for i := len(before) - 1; i > 0; i-- {
line := errLine - i line := errLine - i
buf.WriteString(formatLineNumber(line, lineColumnWidth)) buf.WriteString(formatLineNumber(line, lineColumnWidth))
@@ -129,6 +131,8 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
buf.WriteRune('\n') buf.WriteRune('\n')
} }
// Write the document line that contains the error.
buf.WriteString(formatLineNumber(errLine, lineColumnWidth)) buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
buf.WriteString("| ") buf.WriteString("| ")
@@ -143,6 +147,10 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
} }
buf.WriteRune('\n') buf.WriteRune('\n')
// Write the line with the error message itself (so it does not have a line
// number).
buf.WriteString(strings.Repeat(" ", lineColumnWidth)) buf.WriteString(strings.Repeat(" ", lineColumnWidth))
buf.WriteString("| ") buf.WriteString("| ")
@@ -157,6 +165,8 @@ func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
buf.WriteString(errMessage) buf.WriteString(errMessage)
} }
// Write the lines of context strictly after the error.
for i := 1; i < len(after); i++ { for i := 1; i < len(after); i++ {
buf.WriteRune('\n') buf.WriteRune('\n')
line := errLine + i line := errLine + i
@@ -230,7 +240,7 @@ forward:
rest = rest[o+1:] rest = rest[o+1:]
o = 0 o = 0
case o == len(rest)-1 && o > 0: case o == len(rest)-1:
// add last line only if it's non-empty // add last line only if it's non-empty
afterLines = append(afterLines, rest) afterLines = append(afterLines, rest)
+14 -9
View File
@@ -12,7 +12,6 @@ import (
//nolint:funlen //nolint:funlen
func TestDecodeError(t *testing.T) { func TestDecodeError(t *testing.T) {
t.Parallel()
examples := []struct { examples := []struct {
desc string desc string
@@ -149,12 +148,19 @@ line 5`,
6| 6|
7| line 4`, 7| line 4`,
}, },
{
desc: "handle remainder of the error line when there is only one line",
doc: [3]string{`P=`, `[`, `#`},
msg: "array is incomplete",
expected: `1| P=[#
| ~ array is incomplete`,
},
} }
for _, e := range examples { for _, e := range examples {
e := e e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
t.Parallel()
b := bytes.Buffer{} b := bytes.Buffer{}
b.Write([]byte(e.doc[0])) b.Write([]byte(e.doc[0]))
start := b.Len() start := b.Len()
@@ -182,7 +188,6 @@ line 5`,
} }
func TestDecodeError_Accessors(t *testing.T) { func TestDecodeError_Accessors(t *testing.T) {
t.Parallel()
e := DecodeError{ e := DecodeError{
message: "foo", message: "foo",
@@ -207,12 +212,12 @@ func ExampleDecodeError() {
fmt.Println(err) fmt.Println(err)
//nolint:errorlint var derr *DecodeError
de := err.(*DecodeError) if errors.As(err, &derr) {
fmt.Println(de.String()) fmt.Println(derr.String())
row, col := derr.Position()
row, col := de.Position() fmt.Println("error occurred at row", row, "column", col)
fmt.Println("error occurred at row", row, "column", col) }
// Output: // Output:
// toml: number must have at least one digit between underscores // toml: number must have at least one digit between underscores
// 1| name = 123__456 // 1| name = 123__456
+100
View File
@@ -0,0 +1,100 @@
package toml_test
import (
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func TestFastSimple(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 TestFastSimpleString(t *testing.T) {
m := map[string]string{}
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
require.NoError(t, err)
require.Equal(t, map[string]string{"a": "hello"}, m)
}
func TestFastSimpleInterface(t *testing.T) {
m := map[string]interface{}{}
err := toml.Unmarshal([]byte(`
a = "hello"
b = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"a": "hello",
"b": int64(42),
}, m)
}
func TestFastMultipartKeyInterface(t *testing.T) {
m := map[string]interface{}{}
err := toml.Unmarshal([]byte(`
a.interim = "test"
a.b.c = "hello"
b = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"a": map[string]interface{}{
"interim": "test",
"b": map[string]interface{}{
"c": "hello",
},
},
"b": int64(42),
}, m)
}
func TestFastExistingMap(t *testing.T) {
m := map[string]interface{}{
"ints": map[string]int{},
}
err := toml.Unmarshal([]byte(`
ints.one = 1
ints.two = 2
strings.yo = "hello"`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"ints": map[string]interface{}{
"one": int64(1),
"two": int64(2),
},
"strings": map[string]interface{}{
"yo": "hello",
},
}, m)
}
func TestFastArrayTable(t *testing.T) {
b := []byte(`
[root]
[[root.nested]]
name = 'Bob'
[[root.nested]]
name = 'Alice'
`)
m := map[string]interface{}{}
err := toml.Unmarshal(b, &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"root": map[string]interface{}{
"nested": []interface{}{
map[string]interface{}{
"name": "Bob",
},
map[string]interface{}{
"name": "Alice",
},
},
},
}, m)
}
+56
View File
@@ -0,0 +1,56 @@
//go:build go1.18 || go1.19
// +build go1.18 go1.19
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)
})
}
+2 -2
View File
@@ -1,5 +1,5 @@
module github.com/pelletier/go-toml/v2 module github.com/pelletier/go-toml/v2
go 1.15 go 1.16
require github.com/stretchr/testify v1.7.0 require github.com/stretchr/testify v1.8.0
+8 -4
View File
@@ -1,11 +1,15 @@
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.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=
+65 -59
View File
@@ -2,23 +2,26 @@ package ast
import ( import (
"fmt" "fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
) )
// Iterator starts uninitialized, you need to call Next() first. // Iterator starts uninitialized, you need to call Next() first.
// //
// For example: // For example:
// //
// it := n.Children() // it := n.Children()
// for it.Next() { // for it.Next() {
// it.Node() // it.Node()
// } // }
type Iterator struct { type Iterator struct {
started bool started bool
node Node node *Node
} }
// Next moves the iterator forward and returns true if points to a node, false // Next moves the iterator forward and returns true if points to a
// otherwise. // node, false otherwise.
func (c *Iterator) Next() bool { func (c *Iterator) Next() bool {
if !c.started { if !c.started {
c.started = true c.started = true
@@ -28,8 +31,14 @@ func (c *Iterator) Next() bool {
return c.node.Valid() 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. // Node returns a copy of the node pointed at by the iterator.
func (c *Iterator) Node() Node { func (c *Iterator) Node() *Node {
return c.node return c.node
} }
@@ -44,65 +53,69 @@ type Root struct {
func (r *Root) Iterator() Iterator { func (r *Root) Iterator() Iterator {
it := Iterator{} it := Iterator{}
if len(r.nodes) > 0 { if len(r.nodes) > 0 {
it.node = r.nodes[0] it.node = &r.nodes[0]
} }
return it return it
} }
func (r *Root) at(idx int) Node { func (r *Root) at(idx Reference) *Node {
// TODO: unsafe to point to the node directly return &r.nodes[idx]
return r.nodes[idx]
} }
// Arrays have one child per element in the array. // Arrays have one child per element in the array. InlineTables have
// InlineTables have one child per key-value pair in the table. // one child per key-value pair in the table. KeyValues have at least
// KeyValues have at least two children. The first one is the value. The // two children. The first one is the value. The rest make a
// rest make a potentially dotted key. // potentially dotted key. Table and Array table have one child per
// Table and Array table have one child per element of the key they // element of the key they represent (same as KeyValue, but without
// represent (same as KeyValue, but without the last node being the value). // the last node being the value).
// children []Node
type Node struct { type Node struct {
Kind Kind Kind Kind
Data []byte // Raw bytes from the input Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input).
// next idx (in the root array). 0 if last of the collection. // References to other nodes, as offsets in the backing array
next int // from this node. References can go backward, so those can be
// child idx (in the root array). 0 if no child. // negative.
child int next int // 0 if last element
// pointer to the root array child int // 0 if no child
root *Root
} }
// Next returns a copy of the next node, or an invalid Node if there is no type Range struct {
// next node. Offset uint32
func (n Node) Next() Node { Length uint32
if n.next <= 0 {
return noNode
}
return n.root.at(n.next)
} }
// Child returns a copy of the first child node of this node. Other children // Next returns a copy of the next node, or an invalid Node if there
// can be accessed calling Next on the first child. // is no next node.
// Returns an invalid Node if there is none. func (n *Node) Next() *Node {
func (n Node) Child() Node { if n.next == 0 {
if n.child <= 0 { return nil
return noNode
} }
return n.root.at(n.child) 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). // Valid returns true if the node's kind is set (not to Invalid).
func (n Node) Valid() bool { func (n *Node) Valid() bool {
return n.Kind != Invalid return n != nil
} }
var noNode = Node{} // Key returns the child nodes making the Key on a supported
// node. Panics otherwise. They are guaranteed to be all be of the
// Key returns the child nodes making the Key on a supported node. Panics // Kind Key. A simple key would return just one element.
// 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 { func (n *Node) Key() Iterator {
switch n.Kind { switch n.Kind {
case KeyValue: case KeyValue:
@@ -119,20 +132,13 @@ func (n *Node) Key() Iterator {
} }
// Value returns a pointer to the value node of a KeyValue. // Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil. // Guaranteed to be non-nil. Panics if not called on a KeyValue node,
// Panics if not called on a KeyValue node, or if the Children are malformed. // or if the Children are malformed.
func (n Node) Value() Node { func (n *Node) Value() *Node {
assertKind(KeyValue, n)
return n.Child() return n.Child()
} }
// Children returns an iterator over a node's children. // Children returns an iterator over a node's children.
func (n Node) Children() Iterator { func (n *Node) Children() Iterator {
return Iterator{node: n.Child()} 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))
}
}
+11 -20
View File
@@ -1,12 +1,11 @@
package ast package ast
type Reference struct { type Reference int
idx int
set bool const InvalidReference Reference = -1
}
func (r Reference) Valid() bool { func (r Reference) Valid() bool {
return r.set return r != InvalidReference
} }
type Builder struct { type Builder struct {
@@ -18,8 +17,8 @@ func (b *Builder) Tree() *Root {
return &b.tree return &b.tree
} }
func (b *Builder) NodeAt(ref Reference) Node { func (b *Builder) NodeAt(ref Reference) *Node {
return b.tree.at(ref.idx) return b.tree.at(ref)
} }
func (b *Builder) Reset() { func (b *Builder) Reset() {
@@ -28,33 +27,25 @@ func (b *Builder) Reset() {
} }
func (b *Builder) Push(n Node) Reference { func (b *Builder) Push(n Node) Reference {
n.root = &b.tree
b.lastIdx = len(b.tree.nodes) b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
return Reference{ return Reference(b.lastIdx)
idx: b.lastIdx,
set: true,
}
} }
func (b *Builder) PushAndChain(n Node) Reference { func (b *Builder) PushAndChain(n Node) Reference {
n.root = &b.tree
newIdx := len(b.tree.nodes) newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 { if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
} }
b.lastIdx = newIdx b.lastIdx = newIdx
return Reference{ return Reference(b.lastIdx)
idx: b.lastIdx,
set: true,
}
} }
func (b *Builder) AttachChild(parent Reference, child Reference) { func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent.idx].child = child.idx b.tree.nodes[parent].child = int(child) - int(parent)
} }
func (b *Builder) Chain(from Reference, to Reference) { func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from.idx].next = to.idx b.tree.nodes[from].next = int(to) - int(from)
} }
+3 -3
View File
@@ -25,9 +25,9 @@ const (
Float Float
Integer Integer
LocalDate LocalDate
LocalTime
LocalDateTime LocalDateTime
DateTime DateTime
Time
) )
func (k Kind) String() string { func (k Kind) String() string {
@@ -58,12 +58,12 @@ func (k Kind) String() string {
return "Integer" return "Integer"
case LocalDate: case LocalDate:
return "LocalDate" return "LocalDate"
case LocalTime:
return "LocalTime"
case LocalDateTime: case LocalDateTime:
return "LocalDateTime" return "LocalDateTime"
case DateTime: case DateTime:
return "DateTime" return "DateTime"
case Time:
return "Time"
} }
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k)) panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
} }
+88
View File
@@ -0,0 +1,88 @@
package cli
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml/v2"
)
type ConvertFn func(r io.Reader, w io.Writer) error
type Program struct {
Usage string
Fn ConvertFn
// Inplace allows the command to take more than one file as argument and
// perform convertion in place on each provided file.
Inplace bool
}
func (p *Program) Execute() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, p.Usage) }
flag.Parse()
os.Exit(p.main(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func (p *Program) main(files []string, input io.Reader, output, error io.Writer) int {
err := p.run(files, input, output)
if err != nil {
var derr *toml.DecodeError
if errors.As(err, &derr) {
fmt.Fprintln(error, derr.String())
row, col := derr.Position()
fmt.Fprintln(error, "error occurred at row", row, "column", col)
} else {
fmt.Fprintln(error, err.Error())
}
return -1
}
return 0
}
func (p *Program) run(files []string, input io.Reader, output io.Writer) error {
if len(files) > 0 {
if p.Inplace {
return p.runAllFilesInPlace(files)
}
f, err := os.Open(files[0])
if err != nil {
return err
}
defer f.Close()
input = f
}
return p.Fn(input, output)
}
func (p *Program) runAllFilesInPlace(files []string) error {
for _, path := range files {
err := p.runFileInPlace(path)
if err != nil {
return err
}
}
return nil
}
func (p *Program) runFileInPlace(path string) error {
in, err := ioutil.ReadFile(path)
if err != nil {
return err
}
out := new(bytes.Buffer)
err = p.Fn(bytes.NewReader(in), out)
if err != nil {
return err
}
return ioutil.WriteFile(path, out.Bytes(), 0600)
}
+172
View File
@@ -0,0 +1,172 @@
package cli
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func processMain(args []string, input io.Reader, stdout, stderr io.Writer, f ConvertFn) int {
p := Program{Fn: f}
return p.main(args, input, stdout, stderr)
}
func TestProcessMainStdin(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, 0, exit)
assert.Empty(t, stdout.String())
assert.Empty(t, stderr.String())
}
func TestProcessMainStdinErr(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
return fmt.Errorf("something bad")
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.NotEmpty(t, stderr.String())
}
func TestProcessMainStdinDecodeErr(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
input := strings.NewReader("this is the input")
exit := processMain([]string{}, input, stdout, stderr, func(r io.Reader, w io.Writer) error {
var v interface{}
return toml.Unmarshal([]byte(`qwe = 001`), &v)
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.Contains(t, stderr.String(), "error occurred at")
}
func TestProcessMainFileExists(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "example")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = tmpfile.Write([]byte(`some data`))
require.NoError(t, err)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
exit := processMain([]string{tmpfile.Name()}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, 0, exit)
assert.Empty(t, stdout.String())
assert.Empty(t, stderr.String())
}
func TestProcessMainFileDoesNotExist(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
exit := processMain([]string{"/lets/hope/this/does/not/exist"}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error {
return nil
})
assert.Equal(t, -1, exit)
assert.Empty(t, stdout.String())
assert.NotEmpty(t, stderr.String())
}
func TestProcessMainFilesInPlace(t *testing.T) {
dir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
path2 := path.Join(dir, "file2")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
require.NoError(t, err)
err = ioutil.WriteFile(path2, []byte("content 2"), 0600)
require.NoError(t, err)
p := Program{
Fn: dummyFileFn,
Inplace: true,
}
exit := p.main([]string{path1, path2}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, 0, exit)
v1, err := ioutil.ReadFile(path1)
require.NoError(t, err)
require.Equal(t, "1", string(v1))
v2, err := ioutil.ReadFile(path2)
require.NoError(t, err)
require.Equal(t, "2", string(v2))
}
func TestProcessMainFilesInPlaceErrRead(t *testing.T) {
p := Program{
Fn: dummyFileFn,
Inplace: true,
}
exit := p.main([]string{"/this/path/is/invalid"}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, -1, exit)
}
func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
dir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
require.NoError(t, err)
p := Program{
Fn: func(io.Reader, io.Writer) error { return fmt.Errorf("oh no") },
Inplace: true,
}
exit := p.main([]string{path1}, os.Stdin, os.Stdout, os.Stderr)
require.Equal(t, -1, exit)
v1, err := ioutil.ReadFile(path1)
require.NoError(t, err)
require.Equal(t, "content 1", string(v1))
}
func dummyFileFn(r io.Reader, w io.Writer) error {
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
v := strings.SplitN(string(b), " ", 2)[1]
_, err = w.Write([]byte(v))
return err
}
@@ -1,4 +1,4 @@
package unsafe package danger
import ( import (
"fmt" "fmt"
@@ -57,3 +57,9 @@ func BytesRange(start []byte, end []byte) []byte {
return start[:l] return start[:l]
} }
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
// TODO: replace with unsafe.Add when Go 1.17 is released
// https://github.com/golang/go/issues/40481
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
}
@@ -1,15 +1,16 @@
package unsafe_test package danger_test
import ( import (
"testing" "testing"
"unsafe"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/unsafe" "github.com/pelletier/go-toml/v2/internal/danger"
) )
func TestUnsafeSubsliceOffsetValid(t *testing.T) { func TestSubsliceOffsetValid(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
test func() ([]byte, []byte) test func() ([]byte, []byte)
@@ -28,13 +29,13 @@ func TestUnsafeSubsliceOffsetValid(t *testing.T) {
for _, e := range examples { for _, e := range examples {
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
d, s := e.test() d, s := e.test()
offset := unsafe.SubsliceOffset(d, s) offset := danger.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset) assert.Equal(t, e.offset, offset)
}) })
} }
} }
func TestUnsafeSubsliceOffsetInvalid(t *testing.T) { func TestSubsliceOffsetInvalid(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
test func() ([]byte, []byte) test func() ([]byte, []byte)
@@ -72,13 +73,22 @@ func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
d, s := e.test() d, s := e.test()
require.Panics(t, func() { require.Panics(t, func() {
unsafe.SubsliceOffset(d, s) danger.SubsliceOffset(d, s)
}) })
}) })
} }
} }
func TestUnsafeBytesRange(t *testing.T) { func TestStride(t *testing.T) {
a := []byte{1, 2, 3, 4}
x := &a[1]
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
require.Equal(t, &a[2], n)
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
require.Equal(t, &a[0], n)
}
func TestBytesRange(t *testing.T) {
type fn = func() ([]byte, []byte) type fn = func() ([]byte, []byte)
examples := []struct { examples := []struct {
desc string desc string
@@ -157,10 +167,10 @@ func TestUnsafeBytesRange(t *testing.T) {
start, end := e.test() start, end := e.test()
if e.expected == nil { if e.expected == nil {
require.Panics(t, func() { require.Panics(t, func() {
unsafe.BytesRange(start, end) danger.BytesRange(start, end)
}) })
} else { } else {
res := unsafe.BytesRange(start, end) res := danger.BytesRange(start, end)
require.Equal(t, e.expected, res) require.Equal(t, e.expected, res)
} }
}) })
+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' marshalTestToml := `title = 'TOML Marshal Testing'
[basic_lists] [basic_lists]
floats = [12.3, 45.6, 78.9] floats = [12.3, 45.6, 78.9]
bools = [true, false, true] bools = [true, false, true]
@@ -89,7 +90,6 @@ name = 'Second'
[subdoc.first] [subdoc.first]
name = 'First' name = 'First'
[basic] [basic]
uint = 5001 uint = 5001
bool = true bool = true
@@ -101,9 +101,9 @@ date = 1979-05-27T07:32:00Z
[[subdoclist]] [[subdoclist]]
name = 'List.First' name = 'List.First'
[[subdoclist]] [[subdoclist]]
name = 'List.Second' name = 'List.Second'
` `
result, err := toml.Marshal(docData) result, err := toml.Marshal(docData)
@@ -117,14 +117,15 @@ func TestBasicMarshalQuotedKey(t *testing.T) {
expected := `'Z.string-àéù' = 'Hello' expected := `'Z.string-àéù' = 'Hello'
'Yfloat-𝟘' = 3.5 'Yfloat-𝟘' = 3.5
['Xsubdoc-àéù'] ['Xsubdoc-àéù']
String2 = 'One' String2 = 'One'
[['W.sublist-𝟘']] [['W.sublist-𝟘']]
String2 = 'Two' String2 = 'Two'
[['W.sublist-𝟘']] [['W.sublist-𝟘']]
String2 = 'Three' String2 = 'Three'
` `
require.Equal(t, string(expected), string(result)) require.Equal(t, string(expected), string(result))
@@ -159,8 +160,8 @@ bool = false
int = 0 int = 0
string = '' string = ''
stringlist = [] stringlist = []
[map]
[map]
` `
require.Equal(t, string(expected), string(result)) require.Equal(t, string(expected), string(result))
@@ -151,6 +151,7 @@ type quotedKeyMarshalTestStruct struct {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{ var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{
String: "Hello", String: "Hello",
@@ -160,6 +161,7 @@ var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var quotedKeyMarshalTestToml = []byte(`"Yfloat-𝟘" = 3.5 var quotedKeyMarshalTestToml = []byte(`"Yfloat-𝟘" = 3.5
"Z.string-àéù" = "Hello" "Z.string-àéù" = "Hello"
@@ -223,11 +225,13 @@ type testSubDoc struct {
unexported int `toml:"shouldntBeHere"` unexported int `toml:"shouldntBeHere"`
} }
var biteMe = "Bite me" var (
var float1 float32 = 12.3 biteMe = "Bite me"
var float2 float32 = 45.6 float1 float32 = 12.3
var float3 float32 = 78.9 float2 float32 = 45.6
var subdoc = testSubDoc{"Second", 0} float3 float32 = 78.9
subdoc = testSubDoc{"Second", 0}
)
var docData = testDoc{ var docData = testDoc{
Title: "TOML Marshal Testing", Title: "TOML Marshal Testing",
@@ -270,6 +274,7 @@ var docData = testDoc{
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var mapTestDoc = testMapDoc{ var mapTestDoc = testMapDoc{
Title: "TOML Marshal Testing", Title: "TOML Marshal Testing",
@@ -382,7 +387,7 @@ var intErrTomls = []string{
} }
func TestErrUnmarshal(t *testing.T) { func TestErrUnmarshal(t *testing.T) {
var errTomls = []string{ errTomls := []string{
"bool = truly\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"", "bool = truly\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:3200Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"", "bool = true\ndate = 1979-05-27T07:3200Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123a4\nint = 5000\nstring = \"Bite me\"", "bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123a4\nint = 5000\nstring = \"Bite me\"",
@@ -455,35 +460,6 @@ func TestEmptytomlUnmarshal(t *testing.T) {
assert.Equal(t, emptyTestData, result) 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"`
}
var 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 { type pointerMarshalTestStruct struct {
Str *string Str *string
List *[]string List *[]string
@@ -496,21 +472,23 @@ type pointerMarshalTestStruct struct {
DblPtr *[]*[]*string DblPtr *[]*[]*string
} }
var pointerStr = "Hello" var (
var pointerList = []string{"Hello back"} pointerStr = "Hello"
var pointerListPtr = []*string{&pointerStr} pointerList = []string{"Hello back"}
var pointerMap = map[string]string{"response": "Goodbye"} pointerListPtr = []*string{&pointerStr}
var pointerMapPtr = map[string]*string{"alternate": &pointerStr} pointerMap = map[string]string{"response": "Goodbye"}
var pointerTestData = pointerMarshalTestStruct{ pointerMapPtr = map[string]*string{"alternate": &pointerStr}
Str: &pointerStr, pointerTestData = pointerMarshalTestStruct{
List: &pointerList, Str: &pointerStr,
ListPtr: &pointerListPtr, List: &pointerList,
Map: &pointerMap, ListPtr: &pointerListPtr,
MapPtr: &pointerMapPtr, Map: &pointerMap,
EmptyStr: nil, MapPtr: &pointerMapPtr,
EmptyList: nil, EmptyStr: nil,
EmptyMap: nil, EmptyList: nil,
} EmptyMap: nil,
}
)
var pointerTestToml = []byte(`List = ["Hello back"] var pointerTestToml = []byte(`List = ["Hello back"]
ListPtr = ["Hello"] ListPtr = ["Hello"]
@@ -538,15 +516,17 @@ func TestUnmarshalTypeMismatch(t *testing.T) {
type nestedMarshalTestStruct struct { type nestedMarshalTestStruct struct {
String [][]string String [][]string
//Struct [][]basicMarshalTestSubStruct // Struct [][]basicMarshalTestSubStruct
StringPtr *[]*[]*string StringPtr *[]*[]*string
// StructPtr *[]*[]*basicMarshalTestSubStruct // StructPtr *[]*[]*basicMarshalTestSubStruct
} }
var str1 = "Three" var (
var str2 = "Four" str1 = "Three"
var strPtr = []*string{&str1, &str2} str2 = "Four"
var strPtr2 = []*[]*string{&strPtr} strPtr = []*string{&str1, &str2}
strPtr2 = []*[]*string{&strPtr}
)
var nestedTestData = nestedMarshalTestStruct{ var nestedTestData = nestedMarshalTestStruct{
String: [][]string{{"Five", "Six"}, {"One", "Two"}}, String: [][]string{{"Five", "Six"}, {"One", "Two"}},
@@ -582,10 +562,12 @@ func (c customMarshaler) MarshalTOML() ([]byte, error) {
var customMarshalerData = customMarshaler{FirstName: "Sally", LastName: "Fields"} var customMarshalerData = customMarshaler{FirstName: "Sally", LastName: "Fields"}
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var customMarshalerToml = []byte(`Sally Fields`) var customMarshalerToml = []byte(`Sally Fields`)
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var nestedCustomMarshalerData = customMarshalerParent{ var nestedCustomMarshalerData = customMarshalerParent{
Self: customMarshaler{FirstName: "Maiku", LastName: "Suteda"}, Self: customMarshaler{FirstName: "Maiku", LastName: "Suteda"},
@@ -593,10 +575,12 @@ var nestedCustomMarshalerData = customMarshalerParent{
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"] var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"]
me = "Maiku Suteda" me = "Maiku Suteda"
`) `)
var nestedCustomMarshalerTomlForUnmarshal = []byte(`[friends] var nestedCustomMarshalerTomlForUnmarshal = []byte(`[friends]
FirstName = "Sally" FirstName = "Sally"
LastName = "Fields"`) LastName = "Fields"`)
@@ -613,11 +597,11 @@ func (x *IntOrString) MarshalTOML() ([]byte, error) {
} }
func TestUnmarshalTextMarshaler(t *testing.T) { func TestUnmarshalTextMarshaler(t *testing.T) {
var nested = struct { nested := struct {
Friends textMarshaler `toml:"friends"` Friends textMarshaler `toml:"friends"`
}{} }{}
var expected = struct { expected := struct {
Friends textMarshaler `toml:"friends"` Friends textMarshaler `toml:"friends"`
}{ }{
Friends: textMarshaler{FirstName: "Sally", LastName: "Fields"}, Friends: textMarshaler{FirstName: "Sally", LastName: "Fields"},
@@ -633,6 +617,7 @@ func TestUnmarshalTextMarshaler(t *testing.T) {
} }
// TODO: Remove nolint once type and methods are used by a test // TODO: Remove nolint once type and methods are used by a test
//
//nolint:unused //nolint:unused
type precedentMarshaler struct { type precedentMarshaler struct {
FirstName string FirstName string
@@ -651,6 +636,7 @@ func (m precedentMarshaler) MarshalTOML() ([]byte, error) {
} }
// TODO: Remove nolint once type and method are used by a test // TODO: Remove nolint once type and method are used by a test
//
//nolint:unused //nolint:unused
type customPointerMarshaler struct { type customPointerMarshaler struct {
FirstName string FirstName string
@@ -663,6 +649,7 @@ func (m *customPointerMarshaler) MarshalTOML() ([]byte, error) {
} }
// TODO: Remove nolint once type and method are used by a test // TODO: Remove nolint once type and method are used by a test
//
//nolint:unused //nolint:unused
type textPointerMarshaler struct { type textPointerMarshaler struct {
FirstName string FirstName string
@@ -675,6 +662,7 @@ func (m *textPointerMarshaler) MarshalText() ([]byte, error) {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var commentTestToml = []byte(` var commentTestToml = []byte(`
# it's a comment on type # it's a comment on type
@@ -712,6 +700,7 @@ type mapsTestStruct struct {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var mapsTestData = mapsTestStruct{ var mapsTestData = mapsTestStruct{
Simple: map[string]string{ Simple: map[string]string{
@@ -735,6 +724,7 @@ var mapsTestData = mapsTestStruct{
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var mapsTestToml = []byte(` var mapsTestToml = []byte(`
[Other] [Other]
@@ -757,6 +747,7 @@ var mapsTestToml = []byte(`
`) `)
// TODO: Remove nolint once type is used by a test // TODO: Remove nolint once type is used by a test
//
//nolint:deadcode,unused //nolint:deadcode,unused
type structArrayNoTag struct { type structArrayNoTag struct {
A struct { A struct {
@@ -766,6 +757,7 @@ type structArrayNoTag struct {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var customTagTestToml = []byte(` var customTagTestToml = []byte(`
[postgres] [postgres]
@@ -780,6 +772,7 @@ var customTagTestToml = []byte(`
`) `)
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var customCommentTagTestToml = []byte(` var customCommentTagTestToml = []byte(`
# db connection # db connection
@@ -793,6 +786,7 @@ var customCommentTagTestToml = []byte(`
`) `)
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var customCommentedTagTestToml = []byte(` var customCommentedTagTestToml = []byte(`
[postgres] [postgres]
@@ -847,6 +841,7 @@ func TestUnmarshalTabInStringAndQuotedKey(t *testing.T) {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var customMultilineTagTestToml = []byte(`int_slice = [ var customMultilineTagTestToml = []byte(`int_slice = [
1, 1,
@@ -856,6 +851,7 @@ var customMultilineTagTestToml = []byte(`int_slice = [
`) `)
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var testDocBasicToml = []byte(` var testDocBasicToml = []byte(`
[document] [document]
@@ -868,12 +864,14 @@ var testDocBasicToml = []byte(`
`) `)
// TODO: Remove nolint once type is used by a test // TODO: Remove nolint once type is used by a test
//
//nolint:deadcode //nolint:deadcode
type testDocCustomTag struct { type testDocCustomTag struct {
Doc testDocBasicsCustomTag `file:"document"` Doc testDocBasicsCustomTag `file:"document"`
} }
// TODO: Remove nolint once type is used by a test // TODO: Remove nolint once type is used by a test
//
//nolint:deadcode //nolint:deadcode
type testDocBasicsCustomTag struct { type testDocBasicsCustomTag struct {
Bool bool `file:"bool_val"` Bool bool `file:"bool_val"`
@@ -886,6 +884,7 @@ type testDocBasicsCustomTag struct {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,varcheck //nolint:deadcode,varcheck
var testDocCustomTagData = testDocCustomTag{ var testDocCustomTagData = testDocCustomTag{
Doc: testDocBasicsCustomTag{ Doc: testDocBasicsCustomTag{
@@ -949,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) { func TestUnmarshalNonPointer(t *testing.T) {
a := 1 a := 1
err := toml.Unmarshal([]byte{}, a) err := toml.Unmarshal([]byte{}, a)
@@ -965,6 +987,7 @@ func TestUnmarshalInvalidPointerKind(t *testing.T) {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused //nolint:deadcode,unused
type testDuration struct { type testDuration struct {
Nanosec time.Duration `toml:"nanosec"` Nanosec time.Duration `toml:"nanosec"`
@@ -979,6 +1002,7 @@ type testDuration struct {
} }
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var testDurationToml = []byte(` var testDurationToml = []byte(`
nanosec = "1ns" nanosec = "1ns"
@@ -993,6 +1017,7 @@ a_string = "15s"
`) `)
// TODO: Remove nolint once var is used by a test // TODO: Remove nolint once var is used by a test
//
//nolint:deadcode,unused,varcheck //nolint:deadcode,unused,varcheck
var testDurationToml2 = []byte(`a_string = "15s" var testDurationToml2 = []byte(`a_string = "15s"
hour = "1h0m0s" hour = "1h0m0s"
@@ -1006,6 +1031,7 @@ sec = "1s"
`) `)
// TODO: Remove nolint once type is used by a test // TODO: Remove nolint once type is used by a test
//
//nolint:deadcode,unused //nolint:deadcode,unused
type testBadDuration struct { type testBadDuration struct {
Val time.Duration `toml:"val"` Val time.Duration `toml:"val"`
@@ -1360,7 +1386,6 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) {
t.Run("unexported field should not be set from toml", func(t *testing.T) { t.Run("unexported field should not be set from toml", func(t *testing.T) {
var actual unexportedFieldPreservationTest var actual unexportedFieldPreservationTest
err := toml.Unmarshal([]byte(doc), &actual) err := toml.Unmarshal([]byte(doc), &actual)
if err != nil { if err != nil {
t.Fatal("did not expect an error") t.Fatal("did not expect an error")
} }
@@ -1394,7 +1419,6 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) {
Nested3: &unexportedFieldPreservationTestNested{"baz", "bax"}, Nested3: &unexportedFieldPreservationTestNested{"baz", "bax"},
} }
err := toml.Unmarshal([]byte(doc), &actual) err := toml.Unmarshal([]byte(doc), &actual)
if err != nil { if err != nil {
t.Fatal("did not expect an error") t.Fatal("did not expect an error")
} }
@@ -1431,7 +1455,6 @@ func TestUnmarshalLocalDate(t *testing.T) {
var obj dateStruct var obj dateStruct
err := toml.Unmarshal([]byte(doc), &obj) err := toml.Unmarshal([]byte(doc), &obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1457,7 +1480,6 @@ func TestUnmarshalLocalDate(t *testing.T) {
var obj dateStruct var obj dateStruct
err := toml.Unmarshal([]byte(doc), &obj) err := toml.Unmarshal([]byte(doc), &obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1484,32 +1506,34 @@ func TestUnmarshalLocalDateTime(t *testing.T) {
name: "normal", name: "normal",
in: "1979-05-27T07:32:00", in: "1979-05-27T07:32:00",
out: toml.LocalDateTime{ out: toml.LocalDateTime{
Date: toml.LocalDate{ LocalDate: toml.LocalDate{
Year: 1979, Year: 1979,
Month: 5, Month: 5,
Day: 27, Day: 27,
}, },
Time: toml.LocalTime{ LocalTime: toml.LocalTime{
Hour: 7, Hour: 7,
Minute: 32, Minute: 32,
Second: 0, Second: 0,
Nanosecond: 0, Nanosecond: 0,
}, },
}}, },
},
{ {
name: "with nanoseconds", name: "with nanoseconds",
in: "1979-05-27T00:32:00.999999", in: "1979-05-27T00:32:00.999999",
out: toml.LocalDateTime{ out: toml.LocalDateTime{
Date: toml.LocalDate{ LocalDate: toml.LocalDate{
Year: 1979, Year: 1979,
Month: 5, Month: 5,
Day: 27, Day: 27,
}, },
Time: toml.LocalTime{ LocalTime: toml.LocalTime{
Hour: 0, Hour: 0,
Minute: 32, Minute: 32,
Second: 0, Second: 0,
Nanosecond: 999999000, Nanosecond: 999999000,
Precision: 6,
}, },
}, },
}, },
@@ -1526,7 +1550,6 @@ func TestUnmarshalLocalDateTime(t *testing.T) {
var obj dateStruct var obj dateStruct
err := toml.Unmarshal([]byte(doc), &obj) err := toml.Unmarshal([]byte(doc), &obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1544,31 +1567,30 @@ func TestUnmarshalLocalDateTime(t *testing.T) {
var obj dateStruct var obj dateStruct
err := toml.Unmarshal([]byte(doc), &obj) err := toml.Unmarshal([]byte(doc), &obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if obj.Date.Year() != example.out.Date.Year { if obj.Date.Year() != example.out.Year {
t.Errorf("expected year %d, got %d", example.out.Date.Year, obj.Date.Year()) t.Errorf("expected year %d, got %d", example.out.Year, obj.Date.Year())
} }
if obj.Date.Month() != example.out.Date.Month { if obj.Date.Month() != time.Month(example.out.Month) {
t.Errorf("expected month %d, got %d", example.out.Date.Month, obj.Date.Month()) t.Errorf("expected month %d, got %d", example.out.Month, obj.Date.Month())
} }
if obj.Date.Day() != example.out.Date.Day { if obj.Date.Day() != example.out.Day {
t.Errorf("expected day %d, got %d", example.out.Date.Day, obj.Date.Day()) t.Errorf("expected day %d, got %d", example.out.Day, obj.Date.Day())
} }
if obj.Date.Hour() != example.out.Time.Hour { if obj.Date.Hour() != example.out.Hour {
t.Errorf("expected hour %d, got %d", example.out.Time.Hour, obj.Date.Hour()) t.Errorf("expected hour %d, got %d", example.out.Hour, obj.Date.Hour())
} }
if obj.Date.Minute() != example.out.Time.Minute { if obj.Date.Minute() != example.out.Minute {
t.Errorf("expected minute %d, got %d", example.out.Time.Minute, obj.Date.Minute()) t.Errorf("expected minute %d, got %d", example.out.Minute, obj.Date.Minute())
} }
if obj.Date.Second() != example.out.Time.Second { if obj.Date.Second() != example.out.Second {
t.Errorf("expected second %d, got %d", example.out.Time.Second, obj.Date.Second()) t.Errorf("expected second %d, got %d", example.out.Second, obj.Date.Second())
} }
if obj.Date.Nanosecond() != example.out.Time.Nanosecond { if obj.Date.Nanosecond() != example.out.Nanosecond {
t.Errorf("expected nanoseconds %d, got %d", example.out.Time.Nanosecond, obj.Date.Nanosecond()) t.Errorf("expected nanoseconds %d, got %d", example.out.Nanosecond, obj.Date.Nanosecond())
} }
}) })
} }
@@ -1598,6 +1620,7 @@ func TestUnmarshalLocalTime(t *testing.T) {
Minute: 32, Minute: 32,
Second: 0, Second: 0,
Nanosecond: 999999000, Nanosecond: 999999000,
Precision: 6,
}, },
}, },
} }
@@ -1613,7 +1636,6 @@ func TestUnmarshalLocalTime(t *testing.T) {
var obj dateStruct var obj dateStruct
err := toml.Unmarshal([]byte(doc), &obj) err := toml.Unmarshal([]byte(doc), &obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1951,7 +1973,7 @@ func decoder(doc string) *toml.Decoder {
func strictDecoder(doc string) *toml.Decoder { func strictDecoder(doc string) *toml.Decoder {
d := decoder(doc) d := decoder(doc)
d.SetStrict(true) d.DisallowUnknownFields()
return d return d
} }
@@ -2283,8 +2305,7 @@ func (d *durationString) UnmarshalTOML(v interface{}) error {
return nil return nil
} }
type config437Error struct { type config437Error struct{}
}
func (e *config437Error) UnmarshalTOML(v interface{}) error { func (e *config437Error) UnmarshalTOML(v interface{}) error {
return errors.New("expected") return errors.New("expected")
+74
View File
@@ -0,0 +1,74 @@
package testsuite
import (
"fmt"
"math"
"time"
"github.com/pelletier/go-toml/v2"
)
// addTag adds JSON tags to a data structure as expected by toml-test.
func addTag(key string, tomlData interface{}) interface{} {
// Switch on the data type.
switch orig := tomlData.(type) {
default:
//return map[string]interface{}{}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
// A table: we don't need to add any tags, just recurse for every table
// entry.
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = addTag(k, v)
}
return typed
// An array: we don't need to add any tags, just recurse for every table
// entry.
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = addTag("", v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = addTag("", v)
}
return typed
// Datetime: tag as datetime.
case toml.LocalTime:
return tag("time-local", orig.String())
case toml.LocalDate:
return tag("date-local", orig.String())
case toml.LocalDateTime:
return tag("datetime-local", orig.String())
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
// Tag primitive values: bool, string, int, and float64.
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
// Special case for nan since NaN == NaN is false.
if math.IsNaN(orig) {
return tag("float", "nan")
}
return tag("float", fmt.Sprintf("%v", orig))
}
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+244
View File
@@ -0,0 +1,244 @@
package testsuite
import (
"fmt"
"strconv"
"strings"
"testing"
"time"
)
func CmpJSON(t *testing.T, key string, want, have interface{}) {
switch w := want.(type) {
case map[string]interface{}:
cmpJSONMaps(t, key, w, have)
case []interface{}:
cmpJSONArrays(t, key, w, have)
default:
t.Errorf(
"Key '%s' in expected output should be a map or a list of maps, but it's a %T",
key, want)
}
}
func cmpJSONMaps(t *testing.T, key string, want map[string]interface{}, have interface{}) {
haveMap, ok := have.(map[string]interface{})
if !ok {
mismatch(t, key, "table", want, haveMap)
return
}
// Check to make sure both or neither are values.
if isValue(want) && !isValue(haveMap) {
t.Fatalf("Key '%s' is supposed to be a value, but the parser reports it as a table", key)
}
if !isValue(want) && isValue(haveMap) {
t.Fatalf("Key '%s' is supposed to be a table, but the parser reports it as a value", key)
}
if isValue(want) && isValue(haveMap) {
cmpJSONValues(t, key, want, haveMap)
return
}
// Check that the keys of each map are equivalent.
for k := range want {
if _, ok := haveMap[k]; !ok {
bunk := kjoin(key, k)
t.Fatalf("Could not find key '%s' in parser output.", bunk)
}
}
for k := range haveMap {
if _, ok := want[k]; !ok {
bunk := kjoin(key, k)
t.Fatalf("Could not find key '%s' in expected output.", bunk)
}
}
// Okay, now make sure that each value is equivalent.
for k := range want {
CmpJSON(t, kjoin(key, k), want[k], haveMap[k])
}
}
func cmpJSONArrays(t *testing.T, key string, want, have interface{}) {
wantSlice, ok := want.([]interface{})
if !ok {
panic(fmt.Sprintf("'value' should be a JSON array when 'type=array', but it is a %T", want))
}
haveSlice, ok := have.([]interface{})
if !ok {
t.Fatalf("Malformed output from your encoder: 'value' is not a JSON array: %T", have)
}
if len(wantSlice) != len(haveSlice) {
t.Fatalf("Array lengths differ for key '%s':\n"+
" Expected: %d\n"+
" Your encoder: %d",
key, len(wantSlice), len(haveSlice))
}
for i := 0; i < len(wantSlice); i++ {
CmpJSON(t, key, wantSlice[i], haveSlice[i])
}
}
func cmpJSONValues(t *testing.T, key string, want, have map[string]interface{}) {
wantType, ok := want["type"].(string)
if !ok {
panic(fmt.Sprintf("'type' should be a string, but it is a %T", want["type"]))
}
haveType, ok := have["type"].(string)
if !ok {
t.Fatalf("Malformed output from your encoder: 'type' is not a string: %T", have["type"])
}
if wantType != haveType {
valMismatch(t, key, wantType, haveType, want, have)
}
// If this is an array, then we've got to do some work to check equality.
if wantType == "array" {
cmpJSONArrays(t, key, want, have)
return
}
// Atomic values are always strings
wantVal, ok := want["value"].(string)
if !ok {
panic(fmt.Sprintf("'value' %v should be a string, but it is a %[1]T", want["value"]))
}
haveVal, ok := have["value"].(string)
if !ok {
panic(fmt.Sprintf("Malformed output from your encoder: %T is not a string", have["value"]))
}
// Excepting floats and datetimes, other values can be compared as strings.
switch wantType {
case "float":
cmpFloats(t, key, wantVal, haveVal)
case "datetime", "datetime-local", "date-local", "time-local":
cmpAsDatetimes(t, key, wantType, wantVal, haveVal)
default:
cmpAsStrings(t, key, wantVal, haveVal)
}
}
func cmpAsStrings(t *testing.T, key string, want, have string) {
if want != have {
t.Fatalf("Values for key '%s' don't match:\n"+
" Expected: %s\n"+
" Your encoder: %s",
key, want, have)
}
}
func cmpFloats(t *testing.T, key string, want, have string) {
// Special case for NaN, since NaN != NaN.
if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") {
if want != have {
t.Fatalf("Values for key '%s' don't match:\n"+
" Expected: %v\n"+
" Your encoder: %v",
key, want, have)
}
return
}
wantF, err := strconv.ParseFloat(want, 64)
if err != nil {
panic(fmt.Sprintf("Could not read '%s' as a float value for key '%s'", want, key))
}
haveF, err := strconv.ParseFloat(have, 64)
if err != nil {
panic(fmt.Sprintf("Malformed output from your encoder: key '%s' is not a float: '%s'", key, have))
}
if wantF != haveF {
t.Fatalf("Values for key '%s' don't match:\n"+
" Expected: %v\n"+
" Your encoder: %v",
key, wantF, haveF)
}
}
var datetimeRepl = strings.NewReplacer(
" ", "T",
"t", "T",
"z", "Z")
var layouts = map[string]string{
"datetime": time.RFC3339Nano,
"datetime-local": "2006-01-02T15:04:05.999999999",
"date-local": "2006-01-02",
"time-local": "15:04:05",
}
func cmpAsDatetimes(t *testing.T, key string, kind, want, have string) {
layout, ok := layouts[kind]
if !ok {
panic("should never happen")
}
wantT, err := time.Parse(layout, datetimeRepl.Replace(want))
if err != nil {
panic(fmt.Sprintf("Could not read '%s' as a datetime value for key '%s'", want, key))
}
haveT, err := time.Parse(layout, datetimeRepl.Replace(want))
if err != nil {
t.Fatalf("Malformed output from your encoder: key '%s' is not a datetime: '%s'", key, have)
return
}
if !wantT.Equal(haveT) {
t.Fatalf("Values for key '%s' don't match:\n"+
" Expected: %v\n"+
" Your encoder: %v",
key, wantT, haveT)
}
}
func cmpAsDatetimesLocal(t *testing.T, key string, want, have string) {
if datetimeRepl.Replace(want) != datetimeRepl.Replace(have) {
t.Fatalf("Values for key '%s' don't match:\n"+
" Expected: %v\n"+
" Your encoder: %v",
key, want, have)
}
}
func kjoin(old, key string) string {
if len(old) == 0 {
return key
}
return old + "." + key
}
func isValue(m map[string]interface{}) bool {
if len(m) != 2 {
return false
}
if _, ok := m["type"]; !ok {
return false
}
if _, ok := m["value"]; !ok {
return false
}
return true
}
func mismatch(t *testing.T, key string, wantType string, want, have interface{}) {
t.Fatalf("Key '%s' is not an %s but %[4]T:\n"+
" Expected: %#[3]v\n"+
" Your encoder: %#[4]v",
key, wantType, want, have)
}
func valMismatch(t *testing.T, key string, wantType, haveType string, want, have interface{}) {
t.Fatalf("Key '%s' is not an %s but %s:\n"+
" Expected: %#[3]v\n"+
" Your encoder: %#[4]v",
key, wantType, want, have)
}
+69
View File
@@ -0,0 +1,69 @@
package testsuite
import (
"bytes"
"encoding/json"
"fmt"
"github.com/pelletier/go-toml/v2"
)
type parser struct{}
func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
defer func() {
if r := recover(); r != nil {
switch rr := r.(type) {
case error:
retErr = rr
default:
retErr = fmt.Errorf("%s", rr)
}
}
}()
var v interface{}
if err := toml.Unmarshal([]byte(input), &v); err != nil {
return err.Error(), true, nil
}
j, err := json.MarshalIndent(addTag("", v), "", " ")
if err != nil {
return "", false, retErr
}
return string(j), false, retErr
}
func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
defer func() {
if r := recover(); r != nil {
switch rr := r.(type) {
case error:
retErr = rr
default:
retErr = fmt.Errorf("%s", rr)
}
}
}()
var tmp interface{}
err := json.Unmarshal([]byte(input), &tmp)
if err != nil {
return "", false, err
}
rm, err := rmTag(tmp)
if err != nil {
return err.Error(), true, retErr
}
buf := new(bytes.Buffer)
err = toml.NewEncoder(buf).Encode(rm)
if err != nil {
return err.Error(), true, retErr
}
return buf.String(), false, retErr
}
+110
View File
@@ -0,0 +1,110 @@
package testsuite
import (
"fmt"
"strconv"
"time"
)
// Remove JSON tags to a data structure as returned by toml-test.
func rmTag(typedJson interface{}) (interface{}, error) {
// Check if key is in the table m.
in := func(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}
// Switch on the data type.
switch v := typedJson.(type) {
// Object: this can either be a TOML table or a primitive with tags.
case map[string]interface{}:
// This value represents a primitive: remove the tags and return just
// the primitive value.
if len(v) == 2 && in("type", v) && in("value", v) {
ut, err := untag(v)
if err != nil {
return ut, fmt.Errorf("tag.Remove: %w", err)
}
return ut, nil
}
// Table: remove tags on all children.
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
var err error
m[k], err = rmTag(v2)
if err != nil {
return nil, err
}
}
return m, nil
// Array: remove tags from all itenm.
case []interface{}:
a := make([]interface{}, len(v))
for i := range v {
var err error
a[i], err = rmTag(v[i])
if err != nil {
return nil, err
}
}
return a, nil
}
// The top level must be an object or array.
return nil, fmt.Errorf("unrecognized JSON format '%T'", typedJson)
}
// Return a primitive: read the "type" and convert the "value" to that.
func untag(typed map[string]interface{}) (interface{}, error) {
t := typed["type"].(string)
v := typed["value"].(string)
switch t {
case "string":
return v, nil
case "integer":
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return n, nil
case "float":
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return f, nil
case "datetime":
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", false)
case "datetime-local":
return parseTime(v, "2006-01-02T15:04:05.999999999", true)
case "date-local":
return parseTime(v, "2006-01-02", true)
case "time-local":
return parseTime(v, "15:04:05.999999999", true)
case "bool":
switch v {
case "true":
return true, nil
case "false":
return false, nil
}
return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
}
return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
}
func parseTime(v, format string, local bool) (t time.Time, err error) {
if local {
t, err = time.ParseInLocation(format, v, time.Local)
} else {
t, err = time.Parse(format, v)
}
if err != nil {
return time.Time{}, fmt.Errorf("Could not parse %q as a datetime: %w", v, err)
}
return t, nil
}
+50
View File
@@ -0,0 +1,50 @@
// Package testsuite provides helper functions for interoperating with the
// language-agnostic TOML test suite at github.com/BurntSushi/toml-test.
package testsuite
import (
"encoding/json"
"fmt"
"os"
"github.com/pelletier/go-toml/v2"
)
// Marshal is a helpfer function for calling toml.Marshal
//
// Only needed to avoid package import loops.
func Marshal(v interface{}) ([]byte, error) {
return toml.Marshal(v)
}
// Unmarshal is a helper function for calling toml.Unmarshal.
//
// Only needed to avoid package import loops.
func Unmarshal(data []byte, v interface{}) error {
return toml.Unmarshal(data, v)
}
// ValueToTaggedJSON takes a data structure and returns the tagged JSON
// representation.
func ValueToTaggedJSON(doc interface{}) ([]byte, error) {
return json.MarshalIndent(addTag("", doc), "", " ")
}
// DecodeStdin is a helper function for the toml-test binary interface. TOML input
// is read from STDIN and a resulting tagged JSON representation is written to
// STDOUT.
func DecodeStdin() error {
var decoded map[string]interface{}
if err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
return fmt.Errorf("Error decoding TOML: %s", err)
}
j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(addTag("", decoded)); err != nil {
return fmt.Errorf("Error encoding JSON: %s", err)
}
return nil
}
+4 -4
View File
@@ -11,19 +11,19 @@ type KeyTracker struct {
} }
// UpdateTable sets the state of the tracker with the AST table node. // UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node ast.Node) { func (t *KeyTracker) UpdateTable(node *ast.Node) {
t.reset() t.reset()
t.Push(node) t.Push(node)
} }
// UpdateArrayTable sets the state of the tracker with the AST array table 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 *ast.Node) {
t.reset() t.reset()
t.Push(node) t.Push(node)
} }
// Push the given key on the stack. // Push the given key on the stack.
func (t *KeyTracker) Push(node ast.Node) { func (t *KeyTracker) Push(node *ast.Node) {
it := node.Key() it := node.Key()
for it.Next() { for it.Next() {
t.k = append(t.k, string(it.Node().Data)) t.k = append(t.k, string(it.Node().Data))
@@ -31,7 +31,7 @@ func (t *KeyTracker) Push(node ast.Node) {
} }
// Pop key from stack. // Pop key from stack.
func (t *KeyTracker) Pop(node ast.Node) { func (t *KeyTracker) Pop(node *ast.Node) {
it := node.Key() it := node.Key()
for it.Next() { for it.Next() {
t.k = t.k[:len(t.k)-1] t.k = t.k[:len(t.k)-1]
+280 -124
View File
@@ -1,7 +1,9 @@
package tracker package tracker
import ( import (
"bytes"
"fmt" "fmt"
"sync"
"github.com/pelletier/go-toml/v2/internal/ast" "github.com/pelletier/go-toml/v2/internal/ast"
) )
@@ -29,67 +31,132 @@ func (k keyKind) String() string {
panic("missing keyKind string mapping") panic("missing keyKind string mapping")
} }
// SeenTracker tracks which keys have been seen with which TOML type to flag duplicates // SeenTracker tracks which keys have been seen with which TOML type to flag
// and mismatches according to the spec. // duplicates and mismatches according to the spec.
//
// Each node in the visited tree is represented by an entry. Each entry has an
// identifier, which is provided by a counter. Entries are stored in the array
// entries. As new nodes are discovered (referenced for the first time in the
// TOML document), entries are created and appended to the array. An entry
// points to its parent using its id.
//
// To find whether a given key (sequence of []byte) has already been visited,
// the entries are linearly searched, looking for one with the right name and
// parent id.
//
// Given that all keys appear in the document after their parent, it is
// guaranteed that all descendants of a node are stored after the node, this
// speeds up the search process.
//
// When encountering [[array tables]], the descendants of that node are removed
// to allow that branch of the tree to be "rediscovered". To maintain the
// invariant above, the deletion process needs to keep the order of entries.
// This results in more copies in that case.
type SeenTracker struct { type SeenTracker struct {
root *info entries []entry
current *info currentIdx int
} }
type info struct { var pool sync.Pool
parent *info
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 {
// Use -1 to indicate no child or no sibling.
child int
next int
name []byte
kind keyKind kind keyKind
children map[string]*info
explicit bool explicit bool
kv bool
} }
func (i *info) Clear() { // Find the index of the child of parentIdx with key k. Returns -1 if
i.children = nil // 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 -1
} }
func (i *info) Has(k string) (*info, bool) { // Remove all descendants of node at position idx.
c, ok := i.children[k] func (s *SeenTracker) clear(idx int) {
return c, ok if idx >= len(s.entries) {
} return
func (i *info) SetKind(kind keyKind) {
i.kind = kind
}
func (i *info) CreateTable(k string, explicit bool) *info {
return i.createChild(k, tableKind, explicit)
}
func (i *info) CreateArrayTable(k string, explicit bool) *info {
return i.createChild(k, arrayTableKind, explicit)
}
func (i *info) createChild(k string, kind keyKind, explicit bool) *info {
if i.children == nil {
i.children = make(map[string]*info, 1)
} }
x := &info{ for i := s.entries[idx].child; i >= 0; {
parent: i, 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,
name: name,
kind: kind, kind: kind,
explicit: explicit, explicit: explicit,
kv: kv,
} }
i.children[k] = x var idx int
return x 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 func (s *SeenTracker) setExplicitFlag(parentIdx int) {
// that have been seen in previous calls, and validates that types are consistent. for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
func (s *SeenTracker) CheckExpression(node ast.Node) error { if s.entries[i].kv {
if s.root == nil { s.entries[i].explicit = true
s.root = &info{ s.entries[i].kv = false
kind: tableKind,
} }
s.current = s.root 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.
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
if s.entries == nil {
s.reset()
} }
switch node.Kind { switch node.Kind {
case ast.KeyValue: case ast.KeyValue:
return s.checkKeyValue(s.current, node) return s.checkKeyValue(node)
case ast.Table: case ast.Table:
return s.checkTable(node) return s.checkTable(node)
case ast.ArrayTable: case ast.ArrayTable:
@@ -97,104 +164,193 @@ func (s *SeenTracker) CheckExpression(node ast.Node) error {
default: default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind)) panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
} }
} }
func (s *SeenTracker) checkTable(node ast.Node) error {
s.current = s.root func (s *SeenTracker) checkTable(node *ast.Node) error {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
it := node.Key() it := node.Key()
// handle the first parts of the key, excluding the last one
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
// heap, which is not cheap.
for it.Next() { for it.Next() {
if !it.Node().Next().Valid() { if it.IsLast() {
break break
} }
k := string(it.Node().Data) k := it.Node().Data
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key idx := s.find(parentIdx, k)
k := string(it.Node().Data)
i, found := s.current.Has(k) if idx < 0 {
if found { idx = s.create(parentIdx, k, tableKind, false, false)
if i.kind != tableKind {
return fmt.Errorf("toml: key %s should be a table, not a %s", k, i.kind)
}
if i.explicit {
return fmt.Errorf("toml: table %s already exists", k)
}
i.explicit = true
s.current = i
} else {
s.current = s.current.CreateTable(k, true)
}
return nil
}
func (s *SeenTracker) checkArrayTable(node ast.Node) error {
s.current = s.root
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
if !it.Node().Next().Valid() {
break
}
k := string(it.Node().Data)
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key
k := string(it.Node().Data)
info, found := s.current.Has(k)
if found {
if info.kind != arrayTableKind {
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", info.kind, k)
}
info.Clear()
} else {
info = s.current.CreateArrayTable(k, true)
}
s.current = info
return nil
}
func (s *SeenTracker) checkKeyValue(context *info, node ast.Node) error {
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
k := string(it.Node().Data)
child, found := context.Has(k)
if found {
if child.kind != tableKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", k, child.kind)
}
} else { } else {
child = context.CreateTable(k, false) entry := s.entries[idx]
if entry.kind == valueKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
}
} }
context = child parentIdx = idx
} }
if node.Value().Kind == ast.InlineTable { k := it.Node().Data
context.SetKind(tableKind) idx := s.find(parentIdx, k)
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)
}
if s.entries[idx].explicit {
return fmt.Errorf("toml: table %s already exists", string(k))
}
s.entries[idx].explicit = true
} else { } else {
context.SetKind(valueKind) idx = s.create(parentIdx, k, tableKind, true, false)
}
s.currentIdx = idx
return nil
}
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
it := node.Key()
parentIdx := 0
for it.Next() {
if it.IsLast() {
break
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false, false)
} else {
entry := s.entries[idx]
if entry.kind == valueKind {
return 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 {
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))
}
s.clear(idx)
} else {
idx = s.create(parentIdx, k, arrayTableKind, true, false)
}
s.currentIdx = idx
return nil
}
func (s *SeenTracker) checkKeyValue(node *ast.Node) error {
parentIdx := s.currentIdx
it := node.Key()
for it.Next() {
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false, true)
} else {
entry := s.entries[idx]
if it.IsLast() {
return fmt.Errorf("toml: key %s is already defined", string(k))
} else if entry.kind != tableKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
} else if entry.explicit {
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
}
}
parentIdx = idx
}
s.entries[parentIdx].kind = valueKind
value := node.Value()
switch value.Kind {
case ast.InlineTable:
return s.checkInlineTable(value)
case ast.Array:
return s.checkArray(value)
} }
return nil return nil
} }
func (s *SeenTracker) checkArray(node *ast.Node) error {
it := node.Children()
for it.Next() {
n := it.Node()
switch n.Kind {
case ast.InlineTable:
err := s.checkInlineTable(n)
if err != nil {
return err
}
case ast.Array:
err := s.checkArray(n)
if err != nil {
return err
}
}
}
return nil
}
func (s *SeenTracker) checkInlineTable(node *ast.Node) error {
if pool.New == nil {
pool.New = func() interface{} {
return &SeenTracker{}
}
}
s = pool.Get().(*SeenTracker)
s.reset()
it := node.Children()
for it.Next() {
n := it.Node()
err := s.checkKeyValue(n)
if err != nil {
return err
}
}
// 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 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)
}
+79 -259
View File
@@ -1,300 +1,120 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package civil implements types for civil time, a time-zone-independent
// representation of time that follows the rules of the proleptic
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
// minutes.
//
// Because they lack location information, these types do not represent unique
// moments or intervals of time. Use time.Time for that purpose.
package toml package toml
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
) )
// A LocalDate represents a date (year, month, day). // LocalDate represents a calendar day in no specific timezone.
//
// This type does not include location information, and therefore does not
// describe a unique 24-hour timespan.
type LocalDate struct { type LocalDate struct {
Year int // Year (e.g., 2014). Year int
Month time.Month // Month of the year (January = 1, ...). Month int
Day int // Day of the month, starting at 1. Day int
} }
// LocalDateOf returns the LocalDate in which a time occurs in that time's location. // AsTime converts d into a specific time instance at midnight in zone.
func LocalDateOf(t time.Time) LocalDate { func (d LocalDate) AsTime(zone *time.Location) time.Time {
var d LocalDate return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
d.Year, d.Month, d.Day = t.Date()
return d
} }
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents. // String returns RFC 3339 representation of d.
func ParseLocalDate(s string) (LocalDate, error) {
t, err := time.Parse("2006-01-02", s)
if err != nil {
return LocalDate{}, err
}
return LocalDateOf(t), nil
}
// String returns the date in RFC3339 full-date format.
func (d LocalDate) String() string { func (d LocalDate) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
} }
// IsValid reports whether the date is valid. // MarshalText returns RFC 3339 representation of d.
func (d LocalDate) IsValid() bool {
return LocalDateOf(d.In(time.UTC)) == d
}
// In returns the time corresponding to time 00:00:00 of the date in the location.
//
// In is always consistent with time.LocalDate, even when time.LocalDate returns a time
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc)
// and
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc)
// return 23:00:00 on April 30, 1955.
//
// In panics if loc is nil.
func (d LocalDate) In(loc *time.Location) time.Time {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
}
// AddDays returns the date that is n days in the future.
// n can also be negative to go into the past.
func (d LocalDate) AddDays(n int) LocalDate {
return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n))
}
// DaysSince returns the signed number of days between the date and s, not including the end day.
// This is the inverse operation to AddDays.
func (d LocalDate) DaysSince(s LocalDate) (days int) {
// We convert to Unix time so we do not have to worry about leap seconds:
// Unix time increases by exactly 86400 seconds per day.
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
const secondsInADay = 86400
return int(deltaUnix / secondsInADay)
}
// Before reports whether d1 occurs before future date.
func (d LocalDate) Before(future LocalDate) bool {
if d.Year != future.Year {
return d.Year < future.Year
}
if d.Month != future.Month {
return d.Month < future.Month
}
return d.Day < future.Day
}
// After reports whether d1 occurs after past date.
func (d LocalDate) After(past LocalDate) bool {
return past.Before(d)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of d.String().
func (d LocalDate) MarshalText() ([]byte, error) { func (d LocalDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil return []byte(d.String()), nil
} }
// UnmarshalText implements the encoding.TextUnmarshaler interface. // UnmarshalText parses b using RFC 3339 to fill d.
// The date is expected to be a string in a format accepted by ParseLocalDate. func (d *LocalDate) UnmarshalText(b []byte) error {
func (d *LocalDate) UnmarshalText(data []byte) error { res, err := parseLocalDate(b)
var err error if err != nil {
*d, err = ParseLocalDate(string(data)) return err
}
return err *d = res
return nil
} }
// A LocalTime represents a time with nanosecond precision. // LocalTime represents a time of day of no specific day in no specific
// // timezone.
// This type does not include location information, and therefore does not
// describe a unique moment in time.
//
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type.
type LocalTime struct { type LocalTime struct {
Hour int // The hour of the day in 24-hour format; range [0-23] Hour int // Hour of the day: [0; 24[
Minute int // The minute of the hour; range [0-59] Minute int // Minute of the hour: [0; 60[
Second int // The second of the minute; range [0-59] Second int // Second of the minute: [0; 60[
Nanosecond int // The nanosecond of the second; range [0-999999999] Nanosecond int // Nanoseconds within the second: [0, 1000000000[
Precision int // Number of digits to display for Nanosecond.
} }
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs // String returns RFC 3339 representation of d.
// in that time's location. It ignores the date. // If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
func LocalTimeOf(t time.Time) LocalTime { // component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
var tm LocalTime // of digits for nanoseconds is provided.
tm.Hour, tm.Minute, tm.Second = t.Clock() func (d LocalTime) String() string {
tm.Nanosecond = t.Nanosecond() s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
return tm if d.Precision > 0 {
s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
} else if d.Nanosecond > 0 {
// Nanoseconds are specified, but precision is not provided. Use the
// minimum.
s += strings.Trim(fmt.Sprintf(".%09d", d.Nanosecond), "0")
}
return s
} }
// ParseLocalTime parses a string and returns the time value it represents. // MarshalText returns RFC 3339 representation of d.
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After func (d LocalTime) MarshalText() ([]byte, error) {
// the HH:MM:SS part of the string, an optional fractional part may appear, return []byte(d.String()), nil
// consisting of a decimal point followed by one to nine decimal digits. }
// (RFC3339 admits only one digit after the decimal point).
func ParseLocalTime(s string) (LocalTime, error) { // UnmarshalText parses b using RFC 3339 to fill d.
t, err := time.Parse("15:04:05.999999999", s) func (d *LocalTime) UnmarshalText(b []byte) error {
res, left, err := parseLocalTime(b)
if err == nil && len(left) != 0 {
err = newDecodeError(left, "extra characters")
}
if err != nil { if err != nil {
return LocalTime{}, err return err
} }
*d = res
return LocalTimeOf(t), nil return nil
} }
// String returns the date in the format described in ParseLocalTime. If Nanoseconds // LocalDateTime represents a time of a specific day in no specific timezone.
// is zero, no fractional part will be generated. Otherwise, the result will
// end with a fractional part consisting of a decimal point and nine digits.
func (t LocalTime) String() string {
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
if t.Nanosecond == 0 {
return s
}
return s + fmt.Sprintf(".%09d", t.Nanosecond)
}
// IsValid reports whether the time is valid.
func (t LocalTime) IsValid() bool {
// Construct a non-zero time.
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
return LocalTimeOf(tm) == t
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of t.String().
func (t LocalTime) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The time is expected to be a string in a format accepted by ParseLocalTime.
func (t *LocalTime) UnmarshalText(data []byte) error {
var err error
*t, err = ParseLocalTime(string(data))
return err
}
// A LocalDateTime represents a date and time.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
type LocalDateTime struct { type LocalDateTime struct {
Date LocalDate LocalDate
Time LocalTime LocalTime
} }
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub. // AsTime converts d into a specific time instance in zone.
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
}
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location. // String returns RFC 3339 representation of d.
func LocalDateTimeOf(t time.Time) LocalDateTime { func (d LocalDateTime) String() string {
return LocalDateTime{ return d.LocalDate.String() + "T" + d.LocalTime.String()
Date: LocalDateOf(t), }
Time: LocalTimeOf(t),
// MarshalText returns RFC 3339 representation of d.
func (d LocalDateTime) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalDateTime) UnmarshalText(data []byte) error {
res, left, err := parseLocalDateTime(data)
if err == nil && len(left) != 0 {
err = newDecodeError(left, "extra characters")
} }
}
// ParseLocalDateTime parses a string and returns the LocalDateTime it represents.
// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits
// the time offset but includes an optional fractional time, as described in
// ParseLocalTime. Informally, the accepted format is
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
// where the 'T' may be a lower-case 't'.
func ParseLocalDateTime(s string) (LocalDateTime, error) {
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
if err != nil { if err != nil {
t, err = time.Parse("2006-01-02t15:04:05.999999999", s) return err
if err != nil {
return LocalDateTime{}, err
}
} }
return LocalDateTimeOf(t), nil *d = res
} return nil
// String returns the date in the format described in ParseLocalDate.
func (dt LocalDateTime) String() string {
return dt.Date.String() + "T" + dt.Time.String()
}
// IsValid reports whether the datetime is valid.
func (dt LocalDateTime) IsValid() bool {
return dt.Date.IsValid() && dt.Time.IsValid()
}
// In returns the time corresponding to the LocalDateTime in the given location.
//
// If the time is missing or ambigous at the location, In returns the same
// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then
// both
// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc)
// and
// civil.LocalDateTime{
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}},
// civil.LocalTime{Minute: 30}}.In(loc)
// return 23:30:00 on April 30, 1955.
//
// In panics if loc is nil.
func (dt LocalDateTime) In(loc *time.Location) time.Time {
return time.Date(
dt.Date.Year, dt.Date.Month, dt.Date.Day,
dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc,
)
}
// Before reports whether dt occurs before future.
func (dt LocalDateTime) Before(future LocalDateTime) bool {
return dt.In(time.UTC).Before(future.In(time.UTC))
}
// After reports whether dt occurs after past.
func (dt LocalDateTime) After(past LocalDateTime) bool {
return past.Before(dt)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of dt.String().
func (dt LocalDateTime) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The datetime is expected to be a string in a format accepted by ParseLocalDateTime.
func (dt *LocalDateTime) UnmarshalText(data []byte) error {
var err error
*dt, err = ParseLocalDateTime(string(data))
return err
} }
+83 -472
View File
@@ -1,507 +1,118 @@
// Copyright 2016 Google LLC package toml_test
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package toml
import ( import (
"encoding/json"
"reflect"
"testing" "testing"
"time" "time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
) )
func cmpEqual(x, y interface{}) bool { func TestLocalDate_AsTime(t *testing.T) {
return reflect.DeepEqual(x, y) d := toml.LocalDate{2021, 6, 8}
cast := d.AsTime(time.UTC)
require.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast)
} }
func TestDates(t *testing.T) { func TestLocalDate_String(t *testing.T) {
t.Parallel() d := toml.LocalDate{2021, 6, 8}
require.Equal(t, "2021-06-08", d.String())
for _, test := range []struct {
date LocalDate
loc *time.Location
wantStr string
wantTime time.Time
}{
{
date: LocalDate{2014, 7, 29},
loc: time.Local,
wantStr: "2014-07-29",
wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
},
{
date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
loc: time.UTC,
wantStr: "2014-08-20",
wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
},
{
date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
loc: time.UTC,
wantStr: "0999-01-26",
wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
},
} {
if got := test.date.String(); got != test.wantStr {
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
}
if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
}
}
} }
func TestDateIsValid(t *testing.T) { func TestLocalDate_MarshalText(t *testing.T) {
t.Parallel() d := toml.LocalDate{2021, 6, 8}
b, err := d.MarshalText()
for _, test := range []struct { require.NoError(t, err)
date LocalDate require.Equal(t, []byte("2021-06-08"), b)
want bool
}{
{LocalDate{2014, 7, 29}, true},
{LocalDate{2000, 2, 29}, true},
{LocalDate{10000, 12, 31}, true},
{LocalDate{1, 1, 1}, true},
{LocalDate{0, 1, 1}, true}, // year zero is OK
{LocalDate{-1, 1, 1}, true}, // negative year is OK
{LocalDate{1, 0, 1}, false},
{LocalDate{1, 1, 0}, false},
{LocalDate{2016, 1, 32}, false},
{LocalDate{2016, 13, 1}, false},
{LocalDate{1, -1, 1}, false},
{LocalDate{1, 1, -1}, false},
} {
got := test.date.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.date, got, test.want)
}
}
} }
func TestParseDate(t *testing.T) { func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
t.Parallel() d := toml.LocalDate{}
err := d.UnmarshalText([]byte("2021-06-08"))
require.NoError(t, err)
require.Equal(t, toml.LocalDate{2021, 6, 8}, d)
var emptyDate LocalDate err = d.UnmarshalText([]byte("what"))
require.Error(t, err)
for _, test := range []struct {
str string
want LocalDate // if empty, expect an error
}{
{"2016-01-02", LocalDate{2016, 1, 2}},
{"2016-12-31", LocalDate{2016, 12, 31}},
{"0003-02-04", LocalDate{3, 2, 4}},
{"999-01-26", emptyDate},
{"", emptyDate},
{"2016-01-02x", emptyDate},
} {
got, err := ParseLocalDate(test.str)
if got != test.want {
t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
}
if err != nil && test.want != (emptyDate) {
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
}
}
} }
func TestDateArithmetic(t *testing.T) { func TestLocalTime_String(t *testing.T) {
t.Parallel() d := toml.LocalTime{20, 12, 1, 2, 9}
require.Equal(t, "20:12:01.000000002", d.String())
for _, test := range []struct { d = toml.LocalTime{20, 12, 1, 0, 0}
desc string require.Equal(t, "20:12:01", d.String())
start LocalDate d = toml.LocalTime{20, 12, 1, 0, 9}
end LocalDate require.Equal(t, "20:12:01.000000000", d.String())
days int d = toml.LocalTime{20, 12, 1, 100, 0}
}{ require.Equal(t, "20:12:01.0000001", d.String())
{
desc: "zero days noop",
start: LocalDate{2014, 5, 9},
end: LocalDate{2014, 5, 9},
days: 0,
},
{
desc: "crossing a year boundary",
start: LocalDate{2014, 12, 31},
end: LocalDate{2015, 1, 1},
days: 1,
},
{
desc: "negative number of days",
start: LocalDate{2015, 1, 1},
end: LocalDate{2014, 12, 31},
days: -1,
},
{
desc: "full leap year",
start: LocalDate{2004, 1, 1},
end: LocalDate{2005, 1, 1},
days: 366,
},
{
desc: "full non-leap year",
start: LocalDate{2001, 1, 1},
end: LocalDate{2002, 1, 1},
days: 365,
},
{
desc: "crossing a leap second",
start: LocalDate{1972, 6, 30},
end: LocalDate{1972, 7, 1},
days: 1,
},
{
desc: "dates before the unix epoch",
start: LocalDate{101, 1, 1},
end: LocalDate{102, 1, 1},
days: 365,
},
} {
if got := test.start.AddDays(test.days); got != test.end {
t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
}
if got := test.end.DaysSince(test.start); got != test.days {
t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
}
}
} }
func TestDateBefore(t *testing.T) { func TestLocalTime_MarshalText(t *testing.T) {
t.Parallel() d := toml.LocalTime{20, 12, 1, 2, 9}
b, err := d.MarshalText()
for _, test := range []struct { require.NoError(t, err)
d1, d2 LocalDate require.Equal(t, []byte("20:12:01.000000002"), b)
want bool
}{
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true},
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true},
{LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true},
} {
if got := test.d1.Before(test.d2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
} }
func TestDateAfter(t *testing.T) { func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
t.Parallel() d := toml.LocalTime{}
err := d.UnmarshalText([]byte("20:12:01.000000002"))
require.NoError(t, err)
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
for _, test := range []struct { err = d.UnmarshalText([]byte("what"))
d1, d2 LocalDate require.Error(t, err)
want bool
}{ err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false}, require.Error(t, err)
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
} {
if got := test.d1.After(test.d2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
} }
func TestTimeToString(t *testing.T) { func TestLocalTime_RoundTrip(t *testing.T) {
t.Parallel() var d struct{ A toml.LocalTime }
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
for _, test := range []struct { require.NoError(t, err)
str string require.Equal(t, "20:12:01.500", d.A.String())
time LocalTime
roundTrip bool // ParseLocalTime(str).String() == str?
}{
{"13:26:33", LocalTime{13, 26, 33, 0}, true},
{"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true},
{"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true},
{"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false},
{"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false},
} {
gotTime, err := ParseLocalTime(test.str)
if err != nil {
t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
continue
}
if gotTime != test.time {
t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
}
if test.roundTrip {
gotStr := test.time.String()
if gotStr != test.str {
t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str)
}
}
}
} }
func TestTimeOf(t *testing.T) { func TestLocalDateTime_AsTime(t *testing.T) {
t.Parallel() d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
for _, test := range []struct { toml.LocalTime{20, 12, 1, 2, 9},
time time.Time
want LocalTime
}{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}},
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}},
} {
if got := LocalTimeOf(test.time); got != test.want {
t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
} }
cast := d.AsTime(time.UTC)
require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
} }
func TestTimeIsValid(t *testing.T) { func TestLocalDateTime_String(t *testing.T) {
t.Parallel() d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
for _, test := range []struct { toml.LocalTime{20, 12, 1, 2, 9},
time LocalTime
want bool
}{
{LocalTime{0, 0, 0, 0}, true},
{LocalTime{23, 0, 0, 0}, true},
{LocalTime{23, 59, 59, 999999999}, true},
{LocalTime{24, 59, 59, 999999999}, false},
{LocalTime{23, 60, 59, 999999999}, false},
{LocalTime{23, 59, 60, 999999999}, false},
{LocalTime{23, 59, 59, 1000000000}, false},
{LocalTime{-1, 0, 0, 0}, false},
{LocalTime{0, -1, 0, 0}, false},
{LocalTime{0, 0, -1, 0}, false},
{LocalTime{0, 0, 0, -1}, false},
} {
got := test.time.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.time, got, test.want)
}
} }
require.Equal(t, "2021-06-08T20:12:01.000000002", d.String())
} }
func TestDateTimeToString(t *testing.T) { func TestLocalDateTime_MarshalText(t *testing.T) {
t.Parallel() d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
for _, test := range []struct { toml.LocalTime{20, 12, 1, 2, 9},
str string
dateTime LocalDateTime
roundTrip bool // ParseLocalDateTime(str).String() == str?
}{
{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, true},
{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 600}}, true},
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, false},
} {
gotDateTime, err := ParseLocalDateTime(test.str)
if err != nil {
t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
continue
}
if gotDateTime != test.dateTime {
t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
}
if test.roundTrip {
gotStr := test.dateTime.String()
if gotStr != test.str {
t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str)
}
}
} }
b, err := d.MarshalText()
require.NoError(t, err)
require.Equal(t, []byte("2021-06-08T20:12:01.000000002"), b)
} }
func TestParseDateTimeErrors(t *testing.T) { func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
t.Parallel() d := toml.LocalDateTime{}
err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002"))
require.NoError(t, err)
require.Equal(t, toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
toml.LocalTime{20, 12, 1, 2, 9},
}, d)
for _, str := range []string{ err = d.UnmarshalText([]byte("what"))
"", require.Error(t, err)
"2016-03-22", // just a date
"13:26:33", // just a time err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
"2016-03-22 13:26:33", // wrong separating character require.Error(t, err)
"2016-03-22T13:26:33x", // extra at end
} {
if _, err := ParseLocalDateTime(str); err == nil {
t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
}
}
}
func TestDateTimeOf(t *testing.T) {
t.Parallel()
for _, test := range []struct {
time time.Time
want LocalDateTime
}{
{
time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}},
},
{
time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}},
},
} {
if got := LocalDateTimeOf(test.time); got != test.want {
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
}
}
func TestDateTimeIsValid(t *testing.T) {
t.Parallel()
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
for _, test := range []struct {
dt LocalDateTime
want bool
}{
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true},
{LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false},
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false},
} {
got := test.dt.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.dt, got, test.want)
}
}
}
func TestDateTimeIn(t *testing.T) {
t.Parallel()
dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
if got := dt.In(time.UTC); !got.Equal(want) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestDateTimeBefore(t *testing.T) {
t.Parallel()
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.Before(test.dt2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
func TestDateTimeAfter(t *testing.T) {
t.Parallel()
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.After(test.dt2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
func TestMarshalJSON(t *testing.T) {
t.Parallel()
for _, test := range []struct {
value interface{}
want string
}{
{LocalDate{1987, 4, 15}, `"1987-04-15"`},
{LocalTime{18, 54, 2, 0}, `"18:54:02"`},
{LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`},
} {
bgot, err := json.Marshal(test.value)
if err != nil {
t.Fatal(err)
}
if got := string(bgot); got != test.want {
t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
}
}
}
func TestUnmarshalJSON(t *testing.T) {
t.Parallel()
var (
d LocalDate
tm LocalTime
dt LocalDateTime
)
for _, test := range []struct {
data string
ptr interface{}
want interface{}
}{
{`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}},
{`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}},
{`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}},
{`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}},
} {
if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
t.Fatalf("%s: %v", test.data, err)
}
if !cmpEqual(test.ptr, test.want) {
t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
}
}
for _, bad := range []string{
"", `""`, `"bad"`, `"1987-04-15x"`,
`19870415`, // a JSON number
`11987-04-15x`, // not a JSON string
} {
if json.Unmarshal([]byte(bad), &d) == nil {
t.Errorf("%q, LocalDate: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &tm) == nil {
t.Errorf("%q, LocalTime: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &dt) == nil {
t.Errorf("%q, LocalDateTime: got nil, want error", bad)
}
}
} }
+333 -89
View File
@@ -5,11 +5,13 @@ import (
"encoding" "encoding"
"fmt" "fmt"
"io" "io"
"math"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode"
) )
// Marshal serializes a Go value as a TOML document. // Marshal serializes a Go value as a TOML document.
@@ -52,9 +54,10 @@ func NewEncoder(w io.Writer) *Encoder {
// This behavior can be controlled on an individual struct field basis with the // This behavior can be controlled on an individual struct field basis with the
// inline tag: // inline tag:
// //
// MyField `inline:"true"` // MyField `toml:",inline"`
func (enc *Encoder) SetTablesInline(inline bool) { func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
enc.tablesInline = inline enc.tablesInline = inline
return enc
} }
// SetArraysMultiline forces the encoder to emit all arrays with one element per // SetArraysMultiline forces the encoder to emit all arrays with one element per
@@ -62,28 +65,31 @@ func (enc *Encoder) SetTablesInline(inline bool) {
// //
// This behavior can be controlled on an individual struct field basis with the multiline tag: // This behavior can be controlled on an individual struct field basis with the multiline tag:
// //
// MyField `multiline:"true"` // MyField `multiline:"true"`
func (enc *Encoder) SetArraysMultiline(multiline bool) { func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder {
enc.arraysMultiline = multiline enc.arraysMultiline = multiline
return enc
} }
// SetIndentSymbol defines the string that should be used for indentation. The // SetIndentSymbol defines the string that should be used for indentation. The
// provided string is repeated for each indentation level. Defaults to two // provided string is repeated for each indentation level. Defaults to two
// spaces. // spaces.
func (enc *Encoder) SetIndentSymbol(s string) { func (enc *Encoder) SetIndentSymbol(s string) *Encoder {
enc.indentSymbol = s enc.indentSymbol = s
return enc
} }
// SetIndentTables forces the encoder to intent tables and array tables. // 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 enc.indentTables = indent
return enc
} }
// Encode writes a TOML representation of v to the stream. // Encode writes a TOML representation of v to the stream.
// //
// If v cannot be represented to TOML it returns an error. // 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 // A top level slice containing only maps or structs is encoded as [[table
// array]]. // array]].
@@ -98,27 +104,52 @@ func (enc *Encoder) SetIndentTables(indent bool) {
// Intermediate tables are always printed. // Intermediate tables are always printed.
// //
// By default, strings are encoded as literal string, unless they contain either // 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 // a newline character or a single quote. In that case they are emitted as
// strings. // 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 // When encoding structs, fields are encoded in order of definition, with their
// exact name. // 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 // [top1]
// basis:
// //
// toml:"foo" // [top2]
// Changes the name of the key to use for the field to foo. // [top2.child1]
// //
// multiline:"true" // [[array]]
// When the field contains a string, it will be emitted as a quoted
// multi-line TOML string.
// //
// inline:"true" // [[array]]
// When the field would normally be encoded as a table, it is instead // [array.child2]
// encoded as an inline table. //
// # 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.
//
// 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 { func (enc *Encoder) Encode(v interface{}) error {
var ( var (
b []byte b []byte
@@ -146,6 +177,8 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct { type valueOptions struct {
multiline bool multiline bool
omitempty bool
comment string
} }
type encoderCtx struct { type encoderCtx struct {
@@ -195,16 +228,29 @@ func (ctx *encoderCtx) isRoot() bool {
return len(ctx.parentKey) == 0 && !ctx.hasKey return len(ctx.parentKey) == 0 && !ctx.hasKey
} }
//nolint:cyclop,funlen
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if !v.IsZero() { i := v.Interface()
i, ok := v.Interface().(time.Time)
if ok { switch x := i.(type) {
return i.AppendFormat(b, time.RFC3339), nil case time.Time:
if x.Nanosecond() > 0 {
return x.AppendFormat(b, time.RFC3339Nano), nil
} }
return x.AppendFormat(b, time.RFC3339), nil
case LocalTime:
return append(b, x.String()...), nil
case LocalDate:
return append(b, x.String()...), nil
case LocalDateTime:
return append(b, x.String()...), nil
} }
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() { if ctx.isRoot() {
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type()) return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
} }
@@ -244,9 +290,32 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.String: case reflect.String:
b = enc.encodeString(b, v.String(), ctx.options) b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32: case reflect.Float32:
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, f, 'f', -1, 32)
}
case reflect.Float64: case reflect.Float64:
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, f, 'f', -1, 64)
}
case reflect.Bool: case reflect.Bool:
if v.Bool() { if v.Bool() {
b = append(b, "true"...) b = append(b, "true"...)
@@ -254,7 +323,11 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
b = append(b, "false"...) b = append(b, "false"...)
} }
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: 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: case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10) b = strconv.AppendInt(b, v.Int(), 10)
default: default:
@@ -273,19 +346,19 @@ 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) { func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error var err error
if !ctx.hasKey { if !ctx.inline {
panic("caller of encodeKv should have set the key in the context") b = enc.encodeComment(ctx.indent, options.comment, b)
} }
b = enc.indent(ctx.indent, b) b = enc.indent(ctx.indent, b)
b = enc.encodeKey(b, ctx.key)
b, err = enc.encodeKey(b, ctx.key)
if err != nil {
return nil, err
}
b = append(b, " = "...) b = append(b, " = "...)
// create a copy of the context because the value of a KV shouldn't // create a copy of the context because the value of a KV shouldn't
@@ -303,6 +376,54 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
return b, nil return b, nil
} }
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 = '\'' const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte { func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
@@ -314,7 +435,13 @@ func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byt
} }
func needsQuoting(v string) bool { func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t") // TODO: vectorize
for _, b := range []byte(v) {
if b == '\'' || b == '\r' || b == '\n' || invalidAscii(b) {
return true
}
}
return false
} }
// caller should have checked that the string does not contain new lines or ' . // caller should have checked that the string does not contain new lines or ' .
@@ -326,7 +453,6 @@ func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
return b return b
} }
//nolint:cyclop
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte { func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
stringQuote := `"` stringQuote := `"`
@@ -386,7 +512,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
return b return b
} }
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ . // caller should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte { func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...) return append(b, v...)
} }
@@ -396,24 +522,17 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
return b, nil return b, nil
} }
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
b = enc.indent(ctx.indent, b) b = enc.indent(ctx.indent, b)
b = append(b, '[') b = append(b, '[')
var err error b = enc.encodeKey(b, ctx.parentKey[0])
b, err = enc.encodeKey(b, ctx.parentKey[0])
if err != nil {
return nil, err
}
for _, k := range ctx.parentKey[1:] { for _, k := range ctx.parentKey[1:] {
b = append(b, '.') b = append(b, '.')
b = enc.encodeKey(b, k)
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
}
} }
b = append(b, "]\n"...) b = append(b, "]\n"...)
@@ -422,19 +541,19 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error)
} }
//nolint:cyclop //nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) { func (enc *Encoder) encodeKey(b []byte, k string) []byte {
needsQuotation := false needsQuotation := false
cannotUseLiteral := false cannotUseLiteral := false
if len(k) == 0 {
return append(b, "''"...)
}
for _, c := range k { for _, c := range k {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue continue
} }
if c == '\n' {
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
}
if c == literalQuote { if c == literalQuote {
cannotUseLiteral = true cannotUseLiteral = true
} }
@@ -442,13 +561,17 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation = true needsQuotation = true
} }
if needsQuotation && needsQuoting(k) {
cannotUseLiteral = true
}
switch { switch {
case cannotUseLiteral: case cannotUseLiteral:
return enc.encodeQuotedString(false, b, k), nil return enc.encodeQuotedString(false, b, k)
case needsQuotation: case needsQuotation:
return enc.encodeLiteralString(b, k), nil return enc.encodeLiteralString(b, k)
default: default:
return enc.encodeUnquotedKey(b, k), nil return enc.encodeUnquotedKey(b, k)
} }
} }
@@ -502,18 +625,26 @@ type table struct {
} }
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) { func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
for _, e := range t.kvs {
if e.Key == k {
return
}
}
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options}) t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
} }
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) { func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
for _, e := range t.tables {
if e.Key == k {
return
}
}
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options}) t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
} }
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
var t table // TODO: cache this
//nolint:godox
// TODO: cache this?
typ := v.Type() typ := v.Type()
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
fieldType := typ.Field(i) fieldType := typ.Field(i)
@@ -523,45 +654,130 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
k, ok := fieldType.Tag.Lookup("toml") tag := fieldType.Tag.Get("toml")
if !ok {
k = fieldType.Name
}
// special field name to skip field // special field name to skip field
if k == "-" { if tag == "-" {
continue continue
} }
k, opts := parseTag(tag)
if !isValidName(k) {
k = ""
}
f := v.Field(i) f := v.Field(i)
if k == "" {
if fieldType.Anonymous {
if fieldType.Type.Kind() == reflect.Struct {
walkStruct(ctx, t, f)
}
continue
} else {
k = fieldType.Name
}
}
if isNil(f) { if isNil(f) {
continue continue
} }
options := valueOptions{ options := valueOptions{
multiline: fieldBoolTag(fieldType, "multiline"), multiline: opts.multiline,
omitempty: opts.omitempty,
comment: fieldType.Tag.Get("comment"),
} }
inline := fieldBoolTag(fieldType, "inline") if opts.inline || !willConvertToTableOrArrayTable(ctx, f) {
if inline || !willConvertToTableOrArrayTable(ctx, f) {
t.pushKV(k, f, options) t.pushKV(k, f, options)
} else { } else {
t.pushTable(k, f, options) t.pushTable(k, f, options)
} }
} }
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table
walkStruct(ctx, &t, v)
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
func fieldBoolTag(field reflect.StructField, tag string) bool { func (enc *Encoder) encodeComment(indent int, comment string, b []byte) []byte {
x, ok := field.Tag.Lookup(tag) for len(comment) > 0 {
var line string
return ok && x == "true" 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
}
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
}
}
return tag, opts
} }
//nolint:cyclop
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) { func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error var err error
@@ -583,7 +799,13 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
} }
ctx.skipTableHeader = false ctx.skipTableHeader = false
hasNonEmptyKV := false
for _, kv := range t.kvs { for _, kv := range t.kvs {
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true
ctx.setKey(kv.Key) ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value) b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
@@ -594,7 +816,20 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
b = append(b, '\n') b = append(b, '\n')
} }
first := true
for _, table := range t.tables { 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.setKey(table.Key)
ctx.options = table.Options ctx.options = table.Options
@@ -603,8 +838,6 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
b = append(b, '\n')
} }
return b, nil return b, nil
@@ -617,6 +850,10 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
first := true first := true
for _, kv := range t.kvs { for _, kv := range t.kvs {
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if first { if first {
first = false first = false
} else { } else {
@@ -632,7 +869,7 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
} }
if len(t.tables) > 0 { 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, "}"...) b = append(b, "}"...)
@@ -640,10 +877,11 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
return b, nil return b, nil
} }
var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
func willConvertToTable(ctx encoderCtx, v reflect.Value) bool { func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
if v.Type() == timeType || v.Type().Implements(textMarshalerType) { if !v.IsValid() {
return false
}
if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PtrTo(v.Type()).Implements(textMarshalerType)) {
return false return false
} }
@@ -665,6 +903,9 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
} }
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool { func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
if ctx.insideKv {
return false
}
t := v.Type() t := v.Type()
if t.Kind() == reflect.Interface { if t.Kind() == reflect.Interface {
@@ -710,7 +951,6 @@ func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]by
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
ctx.shiftKey() ctx.shiftKey()
var err error
scratch := make([]byte, 0, 64) scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...) scratch = append(scratch, "[["...)
@@ -719,18 +959,22 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
scratch = append(scratch, '.') scratch = append(scratch, '.')
} }
scratch, err = enc.encodeKey(scratch, k) scratch = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
} }
scratch = append(scratch, "]]\n"...) scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true ctx.skipTableHeader = true
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
if i != 0 {
b = append(b, "\n"...)
}
b = append(b, scratch...) b = append(b, scratch...)
var err error
b, err = enc.encode(b, ctx, v.Index(i)) b, err = enc.encode(b, ctx, v.Index(i))
if err != nil { if err != nil {
return nil, err return nil, err
+491 -105
View File
File diff suppressed because it is too large Load Diff
+265 -119
View File
@@ -2,9 +2,10 @@ package toml
import ( import (
"bytes" "bytes"
"strconv" "unicode"
"github.com/pelletier/go-toml/v2/internal/ast" "github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/danger"
) )
type parser struct { type parser struct {
@@ -16,9 +17,25 @@ type parser struct {
first bool first bool
} }
func (p *parser) Range(b []byte) ast.Range {
return ast.Range{
Offset: uint32(danger.SubsliceOffset(p.data, b)),
Length: uint32(len(b)),
}
}
func (p *parser) Raw(raw ast.Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length]
}
func (p *parser) SetRaw(ref ast.Reference, from []byte, to []byte) {
b := danger.BytesRange(from, to)
p.builder.NodeAt(ref).Raw = p.Range(b)
}
func (p *parser) Reset(b []byte) { func (p *parser) Reset(b []byte) {
p.builder.Reset() p.builder.Reset()
p.ref = ast.Reference{} p.ref = ast.InvalidReference
p.data = b p.data = b
p.left = b p.left = b
p.err = nil p.err = nil
@@ -32,7 +49,7 @@ func (p *parser) NextExpression() bool {
} }
p.builder.Reset() p.builder.Reset()
p.ref = ast.Reference{} p.ref = ast.InvalidReference
for { for {
if len(p.left) == 0 || p.err != nil { if len(p.left) == 0 || p.err != nil {
@@ -61,7 +78,7 @@ func (p *parser) NextExpression() bool {
} }
} }
func (p *parser) Expression() ast.Node { func (p *parser) Expression() *ast.Node {
return p.builder.NodeAt(p.ref) return p.builder.NodeAt(p.ref)
} }
@@ -86,7 +103,7 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
// expression = ws [ comment ] // expression = ws [ comment ]
// expression =/ ws keyval ws [ comment ] // expression =/ ws keyval ws [ comment ]
// expression =/ ws table ws [ comment ] // expression =/ ws table ws [ comment ]
var ref ast.Reference ref := ast.InvalidReference
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
@@ -95,9 +112,8 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
} }
if b[0] == '#' { if b[0] == '#' {
_, rest := scanComment(b) _, rest, err := scanComment(b)
return ref, rest, err
return ref, rest, nil
} }
if b[0] == '\n' || b[0] == '\r' { if b[0] == '\n' || b[0] == '\r' {
@@ -118,9 +134,8 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
if len(b) > 0 && b[0] == '#' { if len(b) > 0 && b[0] == '#' {
_, rest := scanComment(b) _, rest, err := scanComment(b)
return ref, rest, err
return ref, rest, nil
} }
return ref, b, nil return ref, b, nil
@@ -142,12 +157,14 @@ func (p *parser) parseArrayTable(b []byte) (ast.Reference, []byte, error) {
ref := p.builder.Push(ast.Node{ ref := p.builder.Push(ast.Node{
Kind: ast.ArrayTable, Kind: ast.ArrayTable,
}) })
start := b
b = b[2:] b = b[2:]
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
k, b, err := p.parseKey(b) k, b, err := p.parseKey(b)
if err != nil { if err != nil {
p.SetRaw(ref, start, b)
return ref, nil, err return ref, nil, err
} }
@@ -156,11 +173,12 @@ func (p *parser) parseArrayTable(b []byte) (ast.Reference, []byte, error) {
b, err = expect(']', b) b, err = expect(']', b)
if err != nil { if err != nil {
p.SetRaw(ref, start, b)
return ref, nil, err return ref, nil, err
} }
b, err = expect(']', b) b, err = expect(']', b)
p.SetRaw(ref, start, b)
return ref, b, err return ref, b, err
} }
@@ -171,12 +189,14 @@ func (p *parser) parseStdTable(b []byte) (ast.Reference, []byte, error) {
ref := p.builder.Push(ast.Node{ ref := p.builder.Push(ast.Node{
Kind: ast.Table, Kind: ast.Table,
}) })
start := b
b = b[1:] b = b[1:]
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
key, b, err := p.parseKey(b) key, b, err := p.parseKey(b)
if err != nil { if err != nil {
p.SetRaw(ref, start, b)
return ref, nil, err return ref, nil, err
} }
@@ -186,6 +206,7 @@ func (p *parser) parseStdTable(b []byte) (ast.Reference, []byte, error) {
b, err = expect(']', b) b, err = expect(']', b)
p.SetRaw(ref, start, b)
return ref, b, err return ref, b, err
} }
@@ -194,10 +215,12 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
ref := p.builder.Push(ast.Node{ ref := p.builder.Push(ast.Node{
Kind: ast.KeyValue, Kind: ast.KeyValue,
}) })
start := b
key, b, err := p.parseKey(b) key, b, err := p.parseKey(b)
if err != nil { if err != nil {
return ast.Reference{}, nil, err p.SetRaw(ref, start, b)
return ast.InvalidReference, b, err
} }
// keyval-sep = ws %x3D ws ; = // keyval-sep = ws %x3D ws ; =
@@ -205,34 +228,38 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
if len(b) == 0 { if len(b) == 0 {
return ast.Reference{}, nil, newDecodeError(b, "expected = after a key, but the document ends there") p.SetRaw(ref, start, b)
return ast.InvalidReference, b, newDecodeError(b, "expected = after a key, but the document ends there")
} }
b, err = expect('=', b) b, err = expect('=', b)
if err != nil { if err != nil {
return ast.Reference{}, nil, err p.SetRaw(ref, start, b)
return ast.InvalidReference, b, err
} }
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
valRef, b, err := p.parseVal(b) valRef, b, err := p.parseVal(b)
if err != nil { if err != nil {
p.SetRaw(ref, start, b)
return ref, b, err return ref, b, err
} }
p.builder.Chain(valRef, key) p.builder.Chain(valRef, key)
p.builder.AttachChild(ref, valRef) p.builder.AttachChild(ref, valRef)
p.SetRaw(ref, start, b)
return ref, b, err return ref, b, err
} }
//nolint:cyclop,funlen //nolint:cyclop,funlen
func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) { func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
// val = string / boolean / array / inline-table / date-time / float / integer // val = string / boolean / array / inline-table / date-time / float / integer
var ref ast.Reference ref := ast.InvalidReference
if len(b) == 0 { if len(b) == 0 {
return ref, nil, newDecodeError(b, "expected value, not eof") return ref, b, newDecodeError(b, "expected value, not eof")
} }
var err error var err error
@@ -240,32 +267,36 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
switch c { switch c {
case '"': case '"':
var raw []byte
var v []byte var v []byte
if scanFollowsMultilineBasicStringDelimiter(b) { if scanFollowsMultilineBasicStringDelimiter(b) {
v, b, err = p.parseMultilineBasicString(b) raw, v, b, err = p.parseMultilineBasicString(b)
} else { } else {
v, b, err = p.parseBasicString(b) raw, v, b, err = p.parseBasicString(b)
} }
if err == nil { if err == nil {
ref = p.builder.Push(ast.Node{ ref = p.builder.Push(ast.Node{
Kind: ast.String, Kind: ast.String,
Raw: p.Range(raw),
Data: v, Data: v,
}) })
} }
return ref, b, err return ref, b, err
case '\'': case '\'':
var raw []byte
var v []byte var v []byte
if scanFollowsMultilineLiteralStringDelimiter(b) { if scanFollowsMultilineLiteralStringDelimiter(b) {
v, b, err = p.parseMultilineLiteralString(b) raw, v, b, err = p.parseMultilineLiteralString(b)
} else { } else {
v, b, err = p.parseLiteralString(b) raw, v, b, err = p.parseLiteralString(b)
} }
if err == nil { if err == nil {
ref = p.builder.Push(ast.Node{ ref = p.builder.Push(ast.Node{
Kind: ast.String, Kind: ast.String,
Raw: p.Range(raw),
Data: v, Data: v,
}) })
} }
@@ -273,23 +304,25 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
return ref, b, err return ref, b, err
case 't': case 't':
if !scanFollowsTrue(b) { if !scanFollowsTrue(b) {
return ref, nil, newDecodeError(atmost(b, 4), "expected 'true'") return ref, b, newDecodeError(atmost(b, 4), "expected 'true'")
} }
ref = p.builder.Push(ast.Node{ ref = p.builder.Push(ast.Node{
Kind: ast.Bool, Kind: ast.Bool,
Data: b[:4], Data: b[:4],
Raw: p.Range(b[:4]),
}) })
return ref, b[4:], nil return ref, b[4:], nil
case 'f': case 'f':
if !scanFollowsFalse(b) { if !scanFollowsFalse(b) {
return ref, nil, newDecodeError(atmost(b, 5), "expected 'false'") return ref, b, newDecodeError(atmost(b, 5), "expected 'false'")
} }
ref = p.builder.Push(ast.Node{ ref = p.builder.Push(ast.Node{
Kind: ast.Bool, Kind: ast.Bool,
Data: b[:5], Data: b[:5],
Raw: p.Range(b[:5]),
}) })
return ref, b[5:], nil return ref, b[5:], nil
@@ -310,13 +343,13 @@ func atmost(b []byte, n int) []byte {
return b[:n] return b[:n]
} }
func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, error) { func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
v, rest, err := scanLiteralString(b) v, rest, err := scanLiteralString(b)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
return v[1 : len(v)-1], rest, nil 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) (ast.Reference, []byte, error) {
@@ -328,6 +361,7 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
parent := p.builder.Push(ast.Node{ parent := p.builder.Push(ast.Node{
Kind: ast.InlineTable, Kind: ast.InlineTable,
}) })
start := b
first := true first := true
@@ -338,7 +372,14 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
var err error var err error
for len(b) > 0 { for len(b) > 0 {
previousB := b
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
if len(b) == 0 {
p.SetRaw(parent, start, b)
return parent, b, newDecodeError(previousB[:1], "inline table is incomplete")
}
if b[0] == '}' { if b[0] == '}' {
break break
} }
@@ -346,7 +387,8 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
if !first { if !first {
b, err = expect(',', b) b, err = expect(',', b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
} }
@@ -355,7 +397,8 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
kv, b, err = p.parseKeyval(b) kv, b, err = p.parseKeyval(b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
if first { if first {
@@ -370,6 +413,7 @@ func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
rest, err := expect('}', b) rest, err := expect('}', b)
p.SetRaw(parent, start, b)
return parent, rest, err return parent, rest, err
} }
@@ -382,6 +426,8 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
// array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ] // array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
// array-sep = %x2C ; , Comma // array-sep = %x2C ; , Comma
// ws-comment-newline = *( wschar / [ comment ] newline ) // ws-comment-newline = *( wschar / [ comment ] newline )
arrayStart := b
start := b
b = b[1:] b = b[1:]
parent := p.builder.Push(ast.Node{ parent := p.builder.Push(ast.Node{
@@ -396,11 +442,13 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
for len(b) > 0 { for len(b) > 0 {
b, err = p.parseOptionalWhitespaceCommentNewline(b) b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
if len(b) == 0 { if len(b) == 0 {
return parent, nil, newDecodeError(b, "array is incomplete") p.SetRaw(parent, start, b)
return parent, b, newDecodeError(arrayStart[:1], "array is incomplete")
} }
if b[0] == ']' { if b[0] == ']' {
@@ -409,14 +457,19 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
if b[0] == ',' { if b[0] == ',' {
if first { if first {
return parent, nil, newDecodeError(b[0:1], "array cannot start with comma") p.SetRaw(parent, start, b)
return parent, b, newDecodeError(b[0:1], "array cannot start with comma")
} }
b = b[1:] b = b[1:]
b, err = p.parseOptionalWhitespaceCommentNewline(b) b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
} else if !first {
p.SetRaw(parent, start, b)
return parent, b, newDecodeError(b[0:1], "array elements must be separated by commas")
} }
// TOML allows trailing commas in arrays. // TOML allows trailing commas in arrays.
@@ -425,10 +478,10 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
} }
var valueRef ast.Reference var valueRef ast.Reference
valueRef, b, err = p.parseVal(b) valueRef, b, err = p.parseVal(b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
if first { if first {
@@ -440,13 +493,16 @@ func (p *parser) parseValArray(b []byte) (ast.Reference, []byte, error) {
b, err = p.parseOptionalWhitespaceCommentNewline(b) b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err p.SetRaw(parent, start, b)
return parent, b, err
} }
first = false first = false
} }
rest, err := expect(']', b) rest, err := expect(']', b)
p.SetRaw(parent, start, rest)
return parent, rest, err return parent, rest, err
} }
@@ -456,7 +512,10 @@ func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error)
b = p.parseWhitespace(b) b = p.parseWhitespace(b)
if len(b) > 0 && b[0] == '#' { if len(b) > 0 && b[0] == '#' {
_, b = scanComment(b) _, b, err = scanComment(b)
if err != nil {
return b, err
}
} }
if len(b) == 0 { if len(b) == 0 {
@@ -466,7 +525,7 @@ func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error)
if b[0] == '\n' || b[0] == '\r' { if b[0] == '\n' || b[0] == '\r' {
b, err = p.parseNewline(b) b, err = p.parseNewline(b)
if err != nil { if err != nil {
return nil, err return b, err
} }
} else { } else {
break break
@@ -476,10 +535,10 @@ func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error)
return b, nil return b, nil
} }
func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, error) { func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) {
token, rest, err := scanMultilineLiteralString(b) token, rest, err := scanMultilineLiteralString(b)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
i := 3 i := 3
@@ -491,11 +550,11 @@ func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, error) {
i += 2 i += 2
} }
return token[i : len(token)-3], rest, err return token, token[i : len(token)-3], rest, err
} }
//nolint:funlen,gocognit,cyclop //nolint:funlen,gocognit,cyclop
func (p *parser) parseMultilineBasicString(b []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 = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim // ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark // ml-basic-string-delim = 3quotation-mark
@@ -506,13 +565,11 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
// mlb-quotes = 1*2quotation-mark // mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii // mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline ) // mlb-escaped-nl = escape ws newline *( wschar / newline )
token, rest, err := scanMultilineBasicString(b) token, escaped, rest, err := scanMultilineBasicString(b)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
var builder bytes.Buffer
i := 3 i := 3
// skip the immediate new line // skip the immediate new line
@@ -522,9 +579,24 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
i += 2 i += 2
} }
// fast path
startIdx := i
endIdx := len(token) - len(`"""`)
if !escaped {
str := token[startIdx:endIdx]
verr := utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
return token, str, rest, nil
}
return nil, nil, rest, newDecodeError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
}
var builder bytes.Buffer
// The scanner ensures that the token starts and ends with quotes and that // The scanner ensures that the token starts and ends with quotes and that
// escapes are balanced. // escapes are balanced.
for ; i < len(token)-3; i++ { for i < len(token)-3 {
c := token[i] c := token[i]
//nolint:nestif //nolint:nestif
@@ -532,17 +604,33 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
// When the last non-whitespace character on a line is an unescaped \, // When the last non-whitespace character on a line is an unescaped \,
// it will be trimmed along with all whitespace (including newlines) up // it will be trimmed along with all whitespace (including newlines) up
// to the next non-whitespace character or closing delimiter. // to the next non-whitespace character or closing delimiter.
if token[i+1] == '\n' || (token[i+1] == '\r' && token[i+2] == '\n') {
i++ // skip the \ isLastNonWhitespaceOnLine := false
j := 1
findEOLLoop:
for ; j < len(token)-3-i; j++ {
switch token[i+j] {
case ' ', '\t':
continue
case '\r':
if token[i+j+1] == '\n' {
continue
}
case '\n':
isLastNonWhitespaceOnLine = true
}
break findEOLLoop
}
if isLastNonWhitespaceOnLine {
i += j
for ; i < len(token)-3; i++ { for ; i < len(token)-3; i++ {
c := token[i] c := token[i]
if !(c == '\n' || c == '\r' || c == ' ' || c == '\t') { if !(c == '\n' || c == '\r' || c == ' ' || c == '\t') {
i-- i--
break break
} }
} }
i++
continue continue
} }
@@ -563,31 +651,38 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
builder.WriteByte('\r') builder.WriteByte('\r')
case 't': case 't':
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u': case 'u':
x, err := hexToString(atmost(token[i+1:], 4), 4) x, err := hexToRune(atmost(token[i+1:], 4), 4)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
builder.WriteRune(x)
builder.WriteString(x)
i += 4 i += 4
case 'U': case 'U':
x, err := hexToString(atmost(token[i+1:], 8), 8) x, err := hexToRune(atmost(token[i+1:], 8), 8)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
builder.WriteString(x) builder.WriteRune(x)
i += 8 i += 8
default: default:
return nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c) return nil, nil, rest, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
} }
i++
} else { } else {
builder.WriteByte(c) size := utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, rest, newDecodeError(token[i:i+1], "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
} }
} }
return builder.Bytes(), rest, nil return token, builder.Bytes(), rest, nil
} }
func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) { func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
@@ -599,13 +694,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
// dotted-key = simple-key 1*( dot-sep simple-key ) // dotted-key = simple-key 1*( dot-sep simple-key )
// //
// dot-sep = ws %x2E ws ; . Period // dot-sep = ws %x2E ws ; . Period
key, b, err := p.parseSimpleKey(b) raw, key, b, err := p.parseSimpleKey(b)
if err != nil { if err != nil {
return ast.Reference{}, nil, err return ast.InvalidReference, b, err
} }
ref := p.builder.Push(ast.Node{ ref := p.builder.Push(ast.Node{
Kind: ast.Key, Kind: ast.Key,
Raw: p.Range(raw),
Data: key, Data: key,
}) })
@@ -614,13 +710,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
if len(b) > 0 && b[0] == '.' { if len(b) > 0 && b[0] == '.' {
b = p.parseWhitespace(b[1:]) b = p.parseWhitespace(b[1:])
key, b, err = p.parseSimpleKey(b) raw, key, b, err = p.parseSimpleKey(b)
if err != nil { if err != nil {
return ref, nil, err return ref, b, err
} }
p.builder.PushAndChain(ast.Node{ p.builder.PushAndChain(ast.Node{
Kind: ast.Key, Kind: ast.Key,
Raw: p.Range(raw),
Data: key, Data: key,
}) })
} else { } else {
@@ -631,14 +728,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
return ref, b, nil return ref, b, nil
} }
func (p *parser) parseSimpleKey(b []byte) (key, rest []byte, err error) { func (p *parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
if len(b) == 0 {
return nil, nil, b, newDecodeError(b, "expected key but found none")
}
// simple-key = quoted-key / unquoted-key // simple-key = quoted-key / unquoted-key
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
// quoted-key = basic-string / literal-string // quoted-key = basic-string / literal-string
if len(b) == 0 {
return nil, nil, newDecodeError(b, "key is incomplete")
}
switch { switch {
case b[0] == '\'': case b[0] == '\'':
return p.parseLiteralString(b) return p.parseLiteralString(b)
@@ -646,14 +743,14 @@ func (p *parser) parseSimpleKey(b []byte) (key, rest []byte, err error) {
return p.parseBasicString(b) return p.parseBasicString(b)
case isUnquotedKeyChar(b[0]): case isUnquotedKeyChar(b[0]):
key, rest = scanUnquotedKey(b) key, rest = scanUnquotedKey(b)
return key, rest, nil return key, key, rest, nil
default: default:
return nil, nil, newDecodeError(b[0:1], "invalid character at start of key: %c", b[0]) return nil, nil, b[1:], newDecodeError(b[0:1], "invalid character at start of key: %c", b[0])
} }
} }
//nolint:funlen,cyclop //nolint:funlen,cyclop
func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) { func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark // basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; " // quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped // basic-char = basic-unescaped / escaped
@@ -668,16 +765,33 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
// escape-seq-char =/ %x74 ; t tab U+0009 // escape-seq-char =/ %x74 ; t tab U+0009
// escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX // escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
// escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX // escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
token, rest, err := scanBasicString(b) token, escaped, rest, err := scanBasicString(b)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
startIdx := len(`"`)
endIdx := len(token) - len(`"`)
// 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 {
str := token[startIdx:endIdx]
verr := utf8TomlValidAlreadyEscaped(str)
if verr.Zero() {
return token, str, rest, nil
}
return nil, nil, rest, newDecodeError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8")
}
i := startIdx
var builder bytes.Buffer var builder bytes.Buffer
// The scanner ensures that the token starts and ends with quotes and that // The scanner ensures that the token starts and ends with quotes and that
// escapes are balanced. // escapes are balanced.
for i := 1; i < len(token)-1; i++ { for i < len(token)-1 {
c := token[i] c := token[i]
if c == '\\' { if c == '\\' {
i++ i++
@@ -696,47 +810,68 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
builder.WriteByte('\r') builder.WriteByte('\r')
case 't': case 't':
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e':
builder.WriteByte(0x1B)
case 'u': case 'u':
x, err := hexToString(token[i+1:len(token)-1], 4) x, err := hexToRune(token[i+1:len(token)-1], 4)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
builder.WriteString(x) builder.WriteRune(x)
i += 4 i += 4
case 'U': case 'U':
x, err := hexToString(token[i+1:len(token)-1], 8) x, err := hexToRune(token[i+1:len(token)-1], 8)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, rest, err
} }
builder.WriteString(x) builder.WriteRune(x)
i += 8 i += 8
default: default:
return nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c) return nil, nil, rest, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
} }
i++
} else { } else {
builder.WriteByte(c) size := utf8ValidNext(token[i:])
if size == 0 {
return nil, nil, rest, newDecodeError(token[i:i+1], "invalid character %#U", c)
}
builder.Write(token[i : i+size])
i += size
} }
} }
return builder.Bytes(), rest, nil return token, builder.Bytes(), rest, nil
} }
func hexToString(b []byte, length int) (string, error) { func hexToRune(b []byte, length int) (rune, error) {
if len(b) < length { if len(b) < length {
return "", newDecodeError(b, "unicode point needs %d character, not %d", length, len(b)) return -1, newDecodeError(b, "unicode point needs %d character, not %d", length, len(b))
} }
b = b[:length] b = b[:length]
//nolint:godox var r uint32
// TODO: slow for i, c := range b {
intcode, err := strconv.ParseInt(string(b), 16, 32) d := uint32(0)
if err != nil { switch {
return "", newDecodeError(b, "couldn't parse hexadecimal number: %w", err) case '0' <= c && c <= '9':
d = uint32(c - '0')
case 'a' <= c && c <= 'f':
d = uint32(c - 'a' + 10)
case 'A' <= c && c <= 'F':
d = uint32(c - 'A' + 10)
default:
return -1, newDecodeError(b[i:i+1], "non-hex character")
}
r = r*16 + d
} }
return string(rune(intcode)), nil if r > unicode.MaxRune || 0xD800 <= r && r < 0xE000 {
return -1, newDecodeError(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 {
@@ -753,27 +888,28 @@ func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, err
switch b[0] { switch b[0] {
case 'i': case 'i':
if !scanFollowsInf(b) { if !scanFollowsInf(b) {
return ast.Reference{}, nil, newDecodeError(atmost(b, 3), "expected 'inf'") return ast.InvalidReference, b, newDecodeError(atmost(b, 3), "expected 'inf'")
} }
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: ast.Float, Kind: ast.Float,
Data: b[:3], Data: b[:3],
Raw: p.Range(b[:3]),
}), b[3:], nil }), b[3:], nil
case 'n': case 'n':
if !scanFollowsNan(b) { if !scanFollowsNan(b) {
return ast.Reference{}, nil, newDecodeError(atmost(b, 3), "expected 'nan'") return ast.InvalidReference, b, newDecodeError(atmost(b, 3), "expected 'nan'")
} }
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: ast.Float, Kind: ast.Float,
Data: b[:3], Data: b[:3],
Raw: p.Range(b[:3]),
}), b[3:], nil }), b[3:], nil
case '+', '-': case '+', '-':
return p.scanIntOrFloat(b) return p.scanIntOrFloat(b)
} }
//nolint:gomnd
if len(b) < 3 { if len(b) < 3 {
return p.scanIntOrFloat(b) return p.scanIntOrFloat(b)
} }
@@ -791,26 +927,17 @@ func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, err
if idx == 2 && c == ':' || (idx == 4 && c == '-') { if idx == 2 && c == ':' || (idx == 4 && c == '-') {
return p.scanDateTime(b) return p.scanDateTime(b)
} }
break
} }
return p.scanIntOrFloat(b) 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) (ast.Reference, []byte, error) {
// scans for contiguous characters in [0-9T:Z.+-], and up to one space if // scans for contiguous characters in [0-9T:Z.+-], and up to one space if
// followed by a digit. // followed by a digit.
hasDate := false
hasTime := false hasTime := false
hasTz := false hasTz := false
seenSpace := false seenSpace := false
@@ -823,17 +950,23 @@ byteLoop:
switch { switch {
case isDigit(c): case isDigit(c):
case c == '-': case c == '-':
const offsetOfTz = 19 hasDate = true
if i == offsetOfTz { const minOffsetOfTz = 8
if i >= minOffsetOfTz {
hasTz = true hasTz = true
} }
case c == 'T' || c == ':' || c == '.': case c == 'T' || c == 't' || c == ':' || c == '.':
hasTime = true hasTime = true
case c == '+' || c == '-' || c == 'Z': case c == '+' || c == '-' || c == 'Z' || c == 'z':
hasTz = true hasTz = true
case c == ' ': case c == ' ':
if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) { if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) {
i += 2 i += 2
// Avoid reaching past the end of the document in case the time
// is malformed. See TestIssue585.
if i >= len(b) {
i--
}
seenSpace = true seenSpace = true
hasTime = true hasTime = true
} else { } else {
@@ -847,10 +980,14 @@ byteLoop:
var kind ast.Kind var kind ast.Kind
if hasTime { if hasTime {
if hasTz { if hasDate {
kind = ast.DateTime if hasTz {
kind = ast.DateTime
} else {
kind = ast.LocalDateTime
}
} else { } else {
kind = ast.LocalDateTime kind = ast.LocalTime
} }
} else { } else {
kind = ast.LocalDate kind = ast.LocalDate
@@ -859,6 +996,7 @@ byteLoop:
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: kind, Kind: kind,
Data: b[:i], Data: b[:i],
Raw: p.Range(b[:i]),
}), b[i:], nil }), b[i:], nil
} }
@@ -866,7 +1004,7 @@ byteLoop:
func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) { func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
i := 0 i := 0
if len(b) > 2 && b[0] == '0' && b[1] != '.' { if len(b) > 2 && b[0] == '0' && b[1] != '.' && b[1] != 'e' && b[1] != 'E' {
var isValidRune validRuneFn var isValidRune validRuneFn
switch b[1] { switch b[1] {
@@ -892,6 +1030,7 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: ast.Integer, Kind: ast.Integer,
Data: b[:i], Data: b[:i],
Raw: p.Range(b[:i]),
}), b[i:], nil }), b[i:], nil
} }
@@ -915,10 +1054,11 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: ast.Float, Kind: ast.Float,
Data: b[:i+3], Data: b[:i+3],
Raw: p.Range(b[:i+3]),
}), b[i+3:], nil }), b[i+3:], nil
} }
return ast.Reference{}, nil, newDecodeError(b[i:i+1], "unexpected character 'i' while scanning for a number") return ast.InvalidReference, b[i:], newDecodeError(b[i:i+1], "unexpected character 'i' while scanning for a number")
} }
if c == 'n' { if c == 'n' {
@@ -926,17 +1066,18 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: ast.Float, Kind: ast.Float,
Data: b[:i+3], Data: b[:i+3],
Raw: p.Range(b[:i+3]),
}), b[i+3:], nil }), b[i+3:], nil
} }
return ast.Reference{}, nil, newDecodeError(b[i:i+1], "unexpected character 'n' while scanning for a number") return ast.InvalidReference, b[i:], newDecodeError(b[i:i+1], "unexpected character 'n' while scanning for a number")
} }
break break
} }
if i == 0 { if i == 0 {
return ast.Reference{}, b, newDecodeError(b, "incomplete number") return ast.InvalidReference, b, newDecodeError(b, "incomplete number")
} }
kind := ast.Integer kind := ast.Integer
@@ -948,6 +1089,7 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
return p.builder.Push(ast.Node{ return p.builder.Push(ast.Node{
Kind: kind, Kind: kind,
Data: b[:i], Data: b[:i],
Raw: p.Range(b[:i]),
}), b[i:], nil }), b[i:], nil
} }
@@ -973,8 +1115,12 @@ func isValidBinaryRune(r byte) bool {
} }
func expect(x byte, b []byte) ([]byte, error) { func expect(x byte, b []byte) ([]byte, error) {
if len(b) == 0 {
return b, newDecodeError(b, "expected character %c but the document ended here", x)
}
if b[0] != x { if b[0] != x {
return nil, newDecodeError(b[0:1], "expected character %U", x) return b, newDecodeError(b[0:1], "expected character %c", x)
} }
return b[1:], nil return b[1:], nil
+101 -7
View File
@@ -1,6 +1,8 @@
package toml package toml
import ( import (
"strconv"
"strings"
"testing" "testing"
"github.com/pelletier/go-toml/v2/internal/ast" "github.com/pelletier/go-toml/v2/internal/ast"
@@ -9,8 +11,6 @@ import (
//nolint:funlen //nolint:funlen
func TestParser_AST_Numbers(t *testing.T) { func TestParser_AST_Numbers(t *testing.T) {
t.Parallel()
examples := []struct { examples := []struct {
desc string desc string
input string input string
@@ -137,7 +137,6 @@ func TestParser_AST_Numbers(t *testing.T) {
for _, e := range examples { for _, e := range examples {
e := e e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
t.Parallel()
p := parser{} p := parser{}
p.Reset([]byte(`A = ` + e.input)) p.Reset([]byte(`A = ` + e.input))
p.NextExpression() p.NextExpression()
@@ -168,7 +167,7 @@ type (
} }
) )
func compareNode(t *testing.T, e astNode, n ast.Node) { func compareNode(t *testing.T, e astNode, n *ast.Node) {
t.Helper() t.Helper()
require.Equal(t, e.Kind, n.Kind) require.Equal(t, e.Kind, n.Kind)
require.Equal(t, e.Data, n.Data) require.Equal(t, e.Data, n.Data)
@@ -200,8 +199,6 @@ func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
//nolint:funlen //nolint:funlen
func TestParser_AST(t *testing.T) { func TestParser_AST(t *testing.T) {
t.Parallel()
examples := []struct { examples := []struct {
desc string desc string
input string input string
@@ -340,7 +337,6 @@ func TestParser_AST(t *testing.T) {
for _, e := range examples { for _, e := range examples {
e := e e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
t.Parallel()
p := parser{} p := parser{}
p.Reset([]byte(e.input)) p.Reset([]byte(e.input))
p.NextExpression() p.NextExpression()
@@ -354,3 +350,101 @@ func TestParser_AST(t *testing.T) {
}) })
} }
} }
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 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())
}
})
}
}
+122 -26
View File
@@ -49,16 +49,21 @@ func scanLiteralString(b []byte) ([]byte, []byte, error) {
// literal-string = apostrophe *literal-char apostrophe // literal-string = apostrophe *literal-char apostrophe
// apostrophe = %x27 ; ' apostrophe // apostrophe = %x27 ; ' apostrophe
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii // literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
for i := 1; i < len(b); i++ { for i := 1; i < len(b); {
switch b[i] { switch b[i] {
case '\'': case '\'':
return b[:i+1], b[i+1:], nil return b[:i+1], b[i+1:], nil
case '\n': case '\n', '\r':
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines") return nil, b[i+1:], newDecodeError(b[i:i+1], "literal strings cannot have new lines")
} }
size := utf8ValidNext(b[i:])
if size == 0 {
return nil, b[i+1:], newDecodeError(b[i:i+1], "invalid character")
}
i += size
} }
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string") return nil, b[len(b):], newDecodeError(b[len(b):], "unterminated literal string")
} }
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) { func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
@@ -70,23 +75,62 @@ func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
// mll-content = mll-char / newline // mll-content = mll-char / newline
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii // mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
// mll-quotes = 1*2apostrophe // mll-quotes = 1*2apostrophe
for i := 3; i < len(b); i++ { for i := 3; i < len(b); {
if b[i] == '\'' && scanFollowsMultilineLiteralStringDelimiter(b[i:]) { switch b[i] {
return b[:i+3], b[i+3:], nil 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.
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, b[i:], newDecodeError(b[i-3:i+1], "''' not allowed in multiline literal string")
}
return b[:i], b[i:], nil
}
case '\r':
if len(b) < i+2 {
return nil, b[i:], newDecodeError(b[len(b):], `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, b[i+2:], newDecodeError(b[i:i+2], `need a \n after \r`)
}
i += 2 // skip the \n
continue
} }
size := utf8ValidNext(b[i:])
if size == 0 {
return nil, b[i:], newDecodeError(b[i:i+1], "invalid character")
}
i += size
} }
return nil, nil, newDecodeError(b[len(b):], `multiline literal string not terminated by '''`) return nil, b[len(b):], newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
} }
func scanWindowsNewline(b []byte) ([]byte, []byte, error) { func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
const lenCRLF = 2 const lenCRLF = 2
if len(b) < lenCRLF { if len(b) < lenCRLF {
return nil, nil, newDecodeError(b, "windows new line expected") return nil, b, newDecodeError(b, "windows new line expected")
} }
if b[1] != '\n' { if b[1] != '\n' {
return nil, nil, newDecodeError(b, `windows new line should be \r\n`) return nil, b[2:], newDecodeError(b, `windows new line should be \r\n`)
} }
return b[:lenCRLF], b[lenCRLF:], nil return b[:lenCRLF], b[lenCRLF:], nil
@@ -106,45 +150,62 @@ func scanWhitespace(b []byte) ([]byte, []byte) {
} }
//nolint:unparam //nolint:unparam
func scanComment(b []byte) ([]byte, []byte) { func scanComment(b []byte) ([]byte, []byte, error) {
// comment-start-symbol = %x23 ; # // comment-start-symbol = %x23 ; #
// non-ascii = %x80-D7FF / %xE000-10FFFF // non-ascii = %x80-D7FF / %xE000-10FFFF
// non-eol = %x09 / %x20-7F / non-ascii // non-eol = %x09 / %x20-7F / non-ascii
// //
// comment = comment-start-symbol *non-eol // comment = comment-start-symbol *non-eol
for i := 1; i < len(b); i++ {
for i := 1; i < len(b); {
if b[i] == '\n' { if b[i] == '\n' {
return b[:i], b[i:] return b[:i], b[i:], nil
} }
if b[i] == '\r' {
if i+1 < len(b) && b[i+1] == '\n' {
return b[:i+1], b[i+1:], nil
}
return nil, nil, newDecodeError(b[i:i+1], "invalid character in comment")
}
size := utf8ValidNext(b[i:])
if size == 0 {
return nil, b[i+1:], newDecodeError(b[i:i+1], "invalid character in comment")
}
i += size
} }
return b, nil return b, b[len(b):], nil
} }
func scanBasicString(b []byte) ([]byte, []byte, error) { func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark // basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; " // quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped // basic-char = basic-unescaped / escaped
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// escaped = escape escape-seq-char // escaped = escape escape-seq-char
for i := 1; i < len(b); i++ { escaped := false
i := 1
for ; i < len(b); i++ {
switch b[i] { switch b[i] {
case '"': case '"':
return b[:i+1], b[i+1:], nil return b[:i+1], escaped, b[i+1:], nil
case '\n': case '\n', '\r':
return nil, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines") return nil, escaped, b[i+1:], newDecodeError(b[i:i+1], "basic strings cannot have new lines")
case '\\': case '\\':
if len(b) < i+2 { if len(b) < i+2 {
return nil, nil, newDecodeError(b[i:i+1], "need a character after \\") return nil, escaped, b[i+1:], newDecodeError(b[i:i+1], "need a character after \\")
} }
escaped = true
i++ // skip the next character i++ // skip the next character
} }
} }
return nil, nil, newDecodeError(b[len(b):], `basic string not terminated by "`) return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `basic string not terminated by "`)
} }
func scanMultilineBasicString(b []byte) ([]byte, []byte, error) { func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body // ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim // ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark // ml-basic-string-delim = 3quotation-mark
@@ -155,19 +216,54 @@ func scanMultilineBasicString(b []byte) ([]byte, []byte, error) {
// mlb-quotes = 1*2quotation-mark // mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii // mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline ) // mlb-escaped-nl = escape ws newline *( wschar / newline )
for i := 3; i < len(b); i++ {
escaped := false
i := 3
for ; i < len(b); i++ {
switch b[i] { switch b[i] {
case '"': case '"':
if scanFollowsMultilineBasicStringDelimiter(b[i:]) { if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil 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.
if i >= len(b) || b[i] != '"' {
return b[:i], escaped, b[i:], nil
}
i++
if i >= len(b) || b[i] != '"' {
return b[:i], escaped, b[i:], nil
}
i++
if i < len(b) && b[i] == '"' {
return nil, escaped, b[i+1:], newDecodeError(b[i-3:i+1], `""" not allowed in multiline basic string`)
}
return b[:i], escaped, b[i:], nil
} }
case '\\': case '\\':
if len(b) < i+2 { if len(b) < i+2 {
return nil, nil, newDecodeError(b[len(b):], "need a character after \\") return nil, escaped, b[len(b):], newDecodeError(b[len(b):], "need a character after \\")
} }
escaped = true
i++ // skip the next character i++ // skip the next character
case '\r':
if len(b) < i+2 {
return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, escaped, b[i+2:], newDecodeError(b[i:i+2], `need a \n after \r`)
}
i++ // skip the \n
} }
} }
return nil, nil, newDecodeError(b[len(b):], `multiline basic string not terminated by """`) return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
} }
+25 -6
View File
@@ -2,6 +2,7 @@ package toml
import ( import (
"github.com/pelletier/go-toml/v2/internal/ast" "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/internal/tracker"
) )
@@ -14,7 +15,7 @@ type strict struct {
missing []decodeError missing []decodeError
} }
func (s *strict) EnterTable(node ast.Node) { func (s *strict) EnterTable(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -22,7 +23,7 @@ func (s *strict) EnterTable(node ast.Node) {
s.key.UpdateTable(node) s.key.UpdateTable(node)
} }
func (s *strict) EnterArrayTable(node ast.Node) { func (s *strict) EnterArrayTable(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -30,7 +31,7 @@ func (s *strict) EnterArrayTable(node ast.Node) {
s.key.UpdateArrayTable(node) s.key.UpdateArrayTable(node)
} }
func (s *strict) EnterKeyValue(node ast.Node) { func (s *strict) EnterKeyValue(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -38,7 +39,7 @@ func (s *strict) EnterKeyValue(node ast.Node) {
s.key.Push(node) s.key.Push(node)
} }
func (s *strict) ExitKeyValue(node ast.Node) { func (s *strict) ExitKeyValue(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -46,7 +47,7 @@ func (s *strict) ExitKeyValue(node ast.Node) {
s.key.Pop(node) s.key.Pop(node)
} }
func (s *strict) MissingTable(node ast.Node) { func (s *strict) MissingTable(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -58,7 +59,7 @@ func (s *strict) MissingTable(node ast.Node) {
}) })
} }
func (s *strict) MissingField(node ast.Node) { func (s *strict) MissingField(node *ast.Node) {
if !s.Enabled { if !s.Enabled {
return return
} }
@@ -86,3 +87,21 @@ func (s *strict) Error(doc []byte) error {
return err return err
} }
func keyLocation(node *ast.Node) []byte {
k := node.Key()
hasOne := k.Next()
if !hasOne {
panic("should not be called with empty key")
}
start := k.Node().Data
end := k.Node().Data
for k.Next() {
end = k.Node().Data
}
return danger.BytesRange(start, end)
}
-536
View File
@@ -1,536 +0,0 @@
package toml
import (
"fmt"
"math"
"reflect"
"strings"
"sync"
)
type target interface {
// Dereferences the target.
get() reflect.Value
// Store a string at the target.
setString(v string)
// Store a boolean at the target
setBool(v bool)
// Store an int64 at the target
setInt64(v int64)
// Store a float64 at the target
setFloat64(v float64)
// Stores any value at the target
set(v reflect.Value)
}
// valueTarget just contains a reflect.Value that can be set.
// It is used for struct fields.
type valueTarget reflect.Value
func (t valueTarget) get() reflect.Value {
return reflect.Value(t)
}
func (t valueTarget) set(v reflect.Value) {
reflect.Value(t).Set(v)
}
func (t valueTarget) setString(v string) {
t.get().SetString(v)
}
func (t valueTarget) setBool(v bool) {
t.get().SetBool(v)
}
func (t valueTarget) setInt64(v int64) {
t.get().SetInt(v)
}
func (t valueTarget) setFloat64(v float64) {
t.get().SetFloat(v)
}
// interfaceTarget wraps an other target to dereference on get.
type interfaceTarget struct {
x target
}
func (t interfaceTarget) get() reflect.Value {
return t.x.get().Elem()
}
func (t interfaceTarget) set(v reflect.Value) {
t.x.set(v)
}
func (t interfaceTarget) setString(v string) {
panic("interface targets should always go through set")
}
func (t interfaceTarget) setBool(v bool) {
panic("interface targets should always go through set")
}
func (t interfaceTarget) setInt64(v int64) {
panic("interface targets should always go through set")
}
func (t interfaceTarget) setFloat64(v float64) {
panic("interface targets should always go through set")
}
// mapTarget targets a specific key of a map.
type mapTarget struct {
v reflect.Value
k reflect.Value
}
func (t mapTarget) get() reflect.Value {
return t.v.MapIndex(t.k)
}
func (t mapTarget) set(v reflect.Value) {
t.v.SetMapIndex(t.k, v)
}
func (t mapTarget) setString(v string) {
t.set(reflect.ValueOf(v))
}
func (t mapTarget) setBool(v bool) {
t.set(reflect.ValueOf(v))
}
func (t mapTarget) setInt64(v int64) {
t.set(reflect.ValueOf(v))
}
func (t mapTarget) setFloat64(v float64) {
t.set(reflect.ValueOf(v))
}
// makes sure that the value pointed at by t is indexable (Slice, Array), or
// dereferences to an indexable (Ptr, Interface).
func ensureValueIndexable(t target) error {
f := t.get()
switch f.Type().Kind() {
case reflect.Slice:
if f.IsNil() {
t.set(reflect.MakeSlice(f.Type(), 0, 0))
return nil
}
case reflect.Interface:
if f.IsNil() || f.Elem().Type() != sliceInterfaceType {
t.set(reflect.MakeSlice(sliceInterfaceType, 0, 0))
return nil
}
case reflect.Ptr:
panic("pointer should have already been dereferenced")
case reflect.Array:
// arrays are always initialized.
default:
return fmt.Errorf("toml: cannot store array in a %s", f.Kind())
}
return nil
}
var (
sliceInterfaceType = reflect.TypeOf([]interface{}{})
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
)
func ensureMapIfInterface(x target) {
v := x.get()
if v.Kind() == reflect.Interface && v.IsNil() {
newElement := reflect.MakeMap(mapStringInterfaceType)
x.set(newElement)
}
}
func setString(t target, v string) error {
f := t.get()
switch f.Kind() {
case reflect.String:
t.setString(v)
case reflect.Interface:
t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("toml: cannot assign string to a %s", f.Kind())
}
return nil
}
func setBool(t target, v bool) error {
f := t.get()
switch f.Kind() {
case reflect.Bool:
t.setBool(v)
case reflect.Interface:
t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("toml: cannot assign boolean to a %s", f.Kind())
}
return nil
}
const (
maxInt = int64(^uint(0) >> 1)
minInt = -maxInt - 1
)
//nolint:funlen,gocognit,cyclop
func setInt64(t target, v int64) error {
f := t.get()
switch f.Kind() {
case reflect.Int64:
t.setInt64(v)
case reflect.Int32:
if v < math.MinInt32 || v > math.MaxInt32 {
return fmt.Errorf("toml: number %d does not fit in an int32", v)
}
t.set(reflect.ValueOf(int32(v)))
return nil
case reflect.Int16:
if v < math.MinInt16 || v > math.MaxInt16 {
return fmt.Errorf("toml: number %d does not fit in an int16", v)
}
t.set(reflect.ValueOf(int16(v)))
case reflect.Int8:
if v < math.MinInt8 || v > math.MaxInt8 {
return fmt.Errorf("toml: number %d does not fit in an int8", v)
}
t.set(reflect.ValueOf(int8(v)))
case reflect.Int:
if v < minInt || v > maxInt {
return fmt.Errorf("toml: number %d does not fit in an int", v)
}
t.set(reflect.ValueOf(int(v)))
case reflect.Uint64:
if v < 0 {
return fmt.Errorf("toml: negative number %d does not fit in an uint64", v)
}
t.set(reflect.ValueOf(uint64(v)))
case reflect.Uint32:
if v < 0 || v > math.MaxUint32 {
return fmt.Errorf("toml: negative number %d does not fit in an uint32", v)
}
t.set(reflect.ValueOf(uint32(v)))
case reflect.Uint16:
if v < 0 || v > math.MaxUint16 {
return fmt.Errorf("toml: negative number %d does not fit in an uint16", v)
}
t.set(reflect.ValueOf(uint16(v)))
case reflect.Uint8:
if v < 0 || v > math.MaxUint8 {
return fmt.Errorf("toml: negative number %d does not fit in an uint8", v)
}
t.set(reflect.ValueOf(uint8(v)))
case reflect.Uint:
if v < 0 {
return fmt.Errorf("toml: negative number %d does not fit in an uint", v)
}
t.set(reflect.ValueOf(uint(v)))
case reflect.Interface:
t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("toml: integer cannot be assigned to %s", f.Kind())
}
return nil
}
func setFloat64(t target, v float64) error {
f := t.get()
switch f.Kind() {
case reflect.Float64:
t.setFloat64(v)
case reflect.Float32:
if v > math.MaxFloat32 {
return fmt.Errorf("toml: number %f does not fit in a float32", v)
}
t.set(reflect.ValueOf(float32(v)))
case reflect.Interface:
t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("toml: float cannot be assigned to %s", f.Kind())
}
return nil
}
// Returns the element at idx of the value pointed at by target, or an error if
// t does not point to an indexable.
// If the target points to an Array and idx is out of bounds, it returns
// (nil, nil) as this is not a fatal error (the unmarshaler will skip).
func elementAt(t target, idx int) target {
f := t.get()
switch f.Kind() {
case reflect.Slice:
//nolint:godox
// TODO: use the idx function argument and avoid alloc if possible.
idx := f.Len()
t.set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem()))
return valueTarget(t.get().Index(idx))
case reflect.Array:
if idx >= f.Len() {
return nil
}
return valueTarget(f.Index(idx))
case reflect.Interface:
// This function is called after ensureValueIndexable, so it's
// guaranteed that f contains an initialized slice.
ifaceElem := f.Elem()
idx := ifaceElem.Len()
newElem := reflect.New(ifaceElem.Type().Elem()).Elem()
newSlice := reflect.Append(ifaceElem, newElem)
t.set(newSlice)
return valueTarget(t.get().Elem().Index(idx))
default:
// Why ensureValueIndexable let it go through?
panic(fmt.Errorf("elementAt received unhandled value type: %s", f.Kind()))
}
}
func (d *decoder) scopeTableTarget(shouldAppend bool, t target, name string) (target, bool, error) {
x := t.get()
switch x.Kind() {
// Kinds that need to recurse
case reflect.Interface:
t := scopeInterface(shouldAppend, t)
return d.scopeTableTarget(shouldAppend, t, name)
case reflect.Ptr:
t := scopePtr(t)
return d.scopeTableTarget(shouldAppend, t, name)
case reflect.Slice:
t := scopeSlice(shouldAppend, t)
shouldAppend = false
return d.scopeTableTarget(shouldAppend, t, name)
case reflect.Array:
t, err := d.scopeArray(shouldAppend, t)
if err != nil {
return t, false, err
}
shouldAppend = false
return d.scopeTableTarget(shouldAppend, t, name)
// Terminal kinds
case reflect.Struct:
return scopeStruct(x, name)
case reflect.Map:
if x.IsNil() {
t.set(reflect.MakeMap(x.Type()))
x = t.get()
}
return scopeMap(x, name)
default:
panic(fmt.Sprintf("can't scope on a %s", x.Kind()))
}
}
func scopeInterface(shouldAppend bool, t target) target {
initInterface(shouldAppend, t)
return interfaceTarget{t}
}
func scopePtr(t target) target {
initPtr(t)
return valueTarget(t.get().Elem())
}
func initPtr(t target) {
x := t.get()
if !x.IsNil() {
return
}
t.set(reflect.New(x.Type().Elem()))
}
// initInterface makes sure that the interface pointed at by the target is not
// nil.
// Returns the target to the initialized value of the target.
func initInterface(shouldAppend bool, t target) {
x := t.get()
if x.Kind() != reflect.Interface {
panic("this should only be called on interfaces")
}
if !x.IsNil() && (x.Elem().Type() == sliceInterfaceType || x.Elem().Type() == mapStringInterfaceType) {
return
}
var newElement reflect.Value
if shouldAppend {
newElement = reflect.MakeSlice(sliceInterfaceType, 0, 0)
} else {
newElement = reflect.MakeMap(mapStringInterfaceType)
}
t.set(newElement)
}
func scopeSlice(shouldAppend bool, t target) target {
v := t.get()
if shouldAppend {
newElem := reflect.New(v.Type().Elem())
newSlice := reflect.Append(v, newElem.Elem())
t.set(newSlice)
v = t.get()
}
return valueTarget(v.Index(v.Len() - 1))
}
func (d *decoder) scopeArray(shouldAppend bool, t target) (target, error) {
v := t.get()
idx := d.arrayIndex(shouldAppend, v)
if idx >= v.Len() {
return nil, fmt.Errorf("toml: impossible to insert element beyond array's size: %d", v.Len())
}
return valueTarget(v.Index(idx)), nil
}
func scopeMap(v reflect.Value, name string) (target, bool, error) {
k := reflect.ValueOf(name)
keyType := v.Type().Key()
if !k.Type().AssignableTo(keyType) {
if !k.Type().ConvertibleTo(keyType) {
return nil, false, fmt.Errorf("toml: cannot convert map key of type %s to expected type %s", k.Type(), keyType)
}
k = k.Convert(keyType)
}
if !v.MapIndex(k).IsValid() {
newElem := reflect.New(v.Type().Elem())
v.SetMapIndex(k, newElem.Elem())
}
return mapTarget{
v: v,
k: k,
}, true, nil
}
type fieldPathsMap = map[string][]int
type fieldPathsCache struct {
m map[reflect.Type]fieldPathsMap
l sync.RWMutex
}
func (c *fieldPathsCache) get(t reflect.Type) (fieldPathsMap, bool) {
c.l.RLock()
paths, ok := c.m[t]
c.l.RUnlock()
return paths, ok
}
func (c *fieldPathsCache) set(t reflect.Type, m fieldPathsMap) {
c.l.Lock()
c.m[t] = m
c.l.Unlock()
}
var globalFieldPathsCache = fieldPathsCache{
m: map[reflect.Type]fieldPathsMap{},
l: sync.RWMutex{},
}
func scopeStruct(v reflect.Value, name string) (target, bool, error) {
//nolint:godox
// TODO: cache this, and reduce allocations
fieldPaths, ok := globalFieldPathsCache.get(v.Type())
if !ok {
fieldPaths = map[string][]int{}
path := make([]int, 0, 16)
var walk func(reflect.Value)
walk = func(v reflect.Value) {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
l := len(path)
path = append(path, i)
f := t.Field(i)
if f.Anonymous {
walk(v.Field(i))
} else if f.PkgPath == "" {
// only consider exported fields
fieldName, ok := f.Tag.Lookup("toml")
if !ok {
fieldName = f.Name
}
pathCopy := make([]int, len(path))
copy(pathCopy, path)
fieldPaths[fieldName] = pathCopy
// extra copy for the case-insensitive match
fieldPaths[strings.ToLower(fieldName)] = pathCopy
}
path = path[:l]
}
}
walk(v)
globalFieldPathsCache.set(v.Type(), fieldPaths)
}
path, ok := fieldPaths[name]
if !ok {
path, ok = fieldPaths[strings.ToLower(name)]
}
if !ok {
return nil, false, nil
}
return valueTarget(v.FieldByIndex(path)), true, nil
}
-207
View File
@@ -1,207 +0,0 @@
package toml
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStructTarget_Ensure(t *testing.T) {
t.Parallel()
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "handle a nil slice of string",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.False(t, v.IsNil())
},
},
{
desc: "handle an existing slice of string",
input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
require.False(t, v.IsNil())
s, ok := v.Interface().([]string)
if !ok {
t.Errorf("interface %v should be castable into []string", s)
return
}
assert.Equal(t, []string{"foo"}, s)
},
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = ensureValueIndexable(target)
v := target.get()
e.test(v, err)
})
}
}
func TestStructTarget_SetString(t *testing.T) {
t.Parallel()
str := "value"
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "sets a string",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.Equal(t, str, v.String())
},
},
{
desc: "fails on a float",
input: reflect.ValueOf(&struct{ A float64 }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
{
desc: "fails on a slice",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = setString(target, str)
v := target.get()
e.test(v, err)
})
}
}
func TestPushNew(t *testing.T) {
t.Parallel()
t.Run("slice of strings", func(t *testing.T) {
t.Parallel()
type Doc struct {
A []string
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n := elementAt(x, 0)
n.setString("hello")
require.Equal(t, []string{"hello"}, d.A)
n = elementAt(x, 1)
n.setString("world")
require.Equal(t, []string{"hello", "world"}, d.A)
})
t.Run("slice of interfaces", func(t *testing.T) {
t.Parallel()
type Doc struct {
A []interface{}
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n := elementAt(x, 0)
require.NoError(t, setString(n, "hello"))
require.Equal(t, []interface{}{"hello"}, d.A)
n = elementAt(x, 1)
require.NoError(t, setString(n, "world"))
require.Equal(t, []interface{}{"hello", "world"}, d.A)
})
}
func TestScope_Struct(t *testing.T) {
t.Parallel()
examples := []struct {
desc string
input reflect.Value
name string
err bool
found bool
idx []int
}{
{
desc: "simple field",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
idx: []int{0},
found: true,
},
{
desc: "fails not-exported field",
input: reflect.ValueOf(&struct{ a string }{}).Elem(),
name: "a",
err: false,
found: false,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
dec := decoder{}
x, found, err := dec.scopeTableTarget(false, valueTarget(e.input), e.name)
assert.Equal(t, e.found, found)
if e.err {
assert.Error(t, err)
}
if found {
x2, ok := x.(valueTarget)
require.True(t, ok)
x2.get()
}
})
}
}
@@ -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")
+23 -117
View File
@@ -1,14 +1,14 @@
//go:generate go run ./cmd/tomltestgen/main.go -o toml_testgen_test.go
// This is a support file for toml_testgen_test.go // This is a support file for toml_testgen_test.go
package toml_test package toml_test
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv"
"testing" "testing"
"time"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/internal/testsuite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -17,10 +17,14 @@ func testgenInvalid(t *testing.T, input string) {
t.Logf("Input TOML:\n%s", input) t.Logf("Input TOML:\n%s", input)
doc := map[string]interface{}{} doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc) err := testsuite.Unmarshal([]byte(input), &doc)
if err == nil { if err == nil {
t.Log(json.Marshal(doc)) out, err := json.Marshal(doc)
if err != nil {
panic("could not marshal map to json")
}
t.Log("JSON output from unmarshal:", string(out))
t.Fatalf("test did not fail") t.Fatalf("test did not fail")
} }
} }
@@ -29,124 +33,26 @@ func testgenValid(t *testing.T, input string, jsonRef string) {
t.Helper() t.Helper()
t.Logf("Input TOML:\n%s", input) t.Logf("Input TOML:\n%s", input)
doc := map[string]interface{}{} // TODO: change this to interface{}
var doc map[string]interface{}
err := toml.Unmarshal([]byte(input), &doc) err := testsuite.Unmarshal([]byte(input), &doc)
if err != nil { if err != nil {
if de, ok := err.(*toml.DecodeError); ok {
t.Logf("%s\n%s", err, de)
}
t.Fatalf("failed parsing toml: %s", err) t.Fatalf("failed parsing toml: %s", err)
} }
j, err := testsuite.ValueToTaggedJSON(doc)
refDoc := testgenBuildRefDoc(jsonRef)
require.Equal(t, refDoc, doc)
out, err := toml.Marshal(doc)
require.NoError(t, err) require.NoError(t, err)
doc2 := map[string]interface{}{} var ref interface{}
err = toml.Unmarshal(out, &doc2) err = json.Unmarshal([]byte(jsonRef), &ref)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, refDoc, doc2) var actual interface{}
} err = json.Unmarshal([]byte(j), &actual)
require.NoError(t, err)
func testgenBuildRefDoc(jsonRef string) map[string]interface{} {
descTree := map[string]interface{}{} testsuite.CmpJSON(t, "", ref, actual)
err := json.Unmarshal([]byte(jsonRef), &descTree)
if err != nil {
panic(fmt.Sprintf("reference doc should be valid JSON: %s", err))
}
doc := testGenTranslateDesc(descTree)
if doc == nil {
return map[string]interface{}{}
}
return doc.(map[string]interface{})
}
//nolint:funlen,gocognit,cyclop
func testGenTranslateDesc(input interface{}) interface{} {
a, ok := input.([]interface{})
if ok {
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
}
return xs
}
d, ok := input.(map[string]interface{})
if !ok {
panic(fmt.Sprintf("input should be valid map[string]: %v", input))
}
var (
dtype string
dvalue interface{}
)
//nolint:nestif
if len(d) == 2 {
dtypeiface, ok := d["type"]
if ok {
dvalue, ok = d["value"]
if ok {
dtype = dtypeiface.(string)
switch dtype {
case "string":
return dvalue.(string)
case "float":
v, err := strconv.ParseFloat(dvalue.(string), 64)
if err != nil {
panic(fmt.Sprintf("invalid float '%s': %s", dvalue, err))
}
return v
case "integer":
v, err := strconv.ParseInt(dvalue.(string), 10, 64)
if err != nil {
panic(fmt.Sprintf("invalid int '%s': %s", dvalue, err))
}
return v
case "bool":
return dvalue.(string) == "true"
case "datetime":
dt, err := time.Parse("2006-01-02T15:04:05Z", dvalue.(string))
if err != nil {
panic(fmt.Sprintf("invalid datetime '%s': %s", dvalue, err))
}
return dt
case "array":
if dvalue == nil {
return nil
}
a := dvalue.([]interface{})
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
}
return xs
}
panic(fmt.Sprintf("unknown type: %s", dtype))
}
}
}
dest := map[string]interface{}{}
for k, v := range d {
dest[k] = testGenTranslateDesc(v)
}
return dest
} }
+1486 -846
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
package toml
import (
"encoding"
"reflect"
"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 stringType = reflect.TypeOf("")
+989 -355
View File
File diff suppressed because it is too large Load Diff
+1687 -263
View File
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
package toml
import (
"unicode/utf8"
)
type utf8Err struct {
Index int
Size int
}
func (u utf8Err) Zero() bool {
return u.Size == 0
}
// Verified that a given string is only made of valid UTF-8 characters allowed
// by the TOML spec:
//
// Any Unicode character may be used except those that must be escaped:
// quotation mark, backslash, and the control characters other than tab (U+0000
// to U+0008, U+000A to U+001F, U+007F).
//
// It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early
// when a character is not allowed.
//
// The returned utf8Err is Zero() if the string is valid, or contains the byte
// index and size of the invalid character.
//
// quotation mark => already checked
// backslash => already checked
// 0-0x8 => invalid
// 0x9 => tab, ok
// 0xA - 0x1F => invalid
// 0x7F => invalid
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 {
// Combining two 32 bit loads allows the same code to be used
// for 32 and 64 bit platforms.
// The compiler can generate a 32bit load for first32 and second32
// on many platforms. See test/codegen/memcombine.go.
first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
if (first32|second32)&0x80808080 != 0 {
// Found a non ASCII byte (>= RuneSelf).
break
}
for i, b := range p[:8] {
if invalidAscii(b) {
err.Index = offset + i
err.Size = 1
return
}
}
p = p[8:]
offset += 8
}
n := len(p)
for i := 0; i < n; {
pi := p[i]
if pi < utf8.RuneSelf {
if invalidAscii(pi) {
err.Index = offset + i
err.Size = 1
return
}
i++
continue
}
x := first[pi]
if x == xx {
// Illegal starter byte.
err.Index = offset + i
err.Size = 1
return
}
size := int(x & 7)
if i+size > n {
// Short or invalid.
err.Index = offset + i
err.Size = n - i
return
}
accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c {
err.Index = offset + i
err.Size = 2
return
} else if size == 2 {
} else if c := p[i+2]; c < locb || hicb < c {
err.Index = offset + i
err.Size = 3
return
} else if size == 3 {
} else if c := p[i+3]; c < locb || hicb < c {
err.Index = offset + i
err.Size = 4
return
}
i += size
}
return
}
// Return the size of the next rune if valid, 0 otherwise.
func utf8ValidNext(p []byte) int {
c := p[0]
if c < utf8.RuneSelf {
if invalidAscii(c) {
return 0
}
return 1
}
x := first[c]
if x == xx {
// Illegal starter byte.
return 0
}
size := int(x & 7)
if size > len(p) {
// Short or invalid.
return 0
}
accept := acceptRanges[x>>4]
if c := p[1]; c < accept.lo || accept.hi < c {
return 0
} else if size == 2 {
} else if c := p[2]; c < locb || hicb < c {
return 0
} else if size == 3 {
} else if c := p[3]; c < locb || hicb < c {
return 0
}
return size
}
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]
}
// acceptRange gives the range of valid values for the second byte in a UTF-8
// sequence.
type acceptRange struct {
lo uint8 // lowest value for second byte.
hi uint8 // highest value for second byte.
}
// acceptRanges has size 16 to avoid bounds checks in the code that uses it.
var acceptRanges = [16]acceptRange{
0: {locb, hicb},
1: {0xA0, hicb},
2: {locb, 0x9F},
3: {0x90, hicb},
4: {locb, 0x8F},
}
// first is information about the first byte in a UTF-8 sequence.
var first = [256]uint8{
// 1 2 3 4 5 6 7 8 9 A B C D E F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
// 1 2 3 4 5 6 7 8 9 A B C D E F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
}
const (
// The default lowest and highest continuation byte.
locb = 0b10000000
hicb = 0b10111111
// These names of these constants are chosen to give nice alignment in the
// table below. The first nibble is an index into acceptRanges or F for
// special one-byte cases. The second nibble is the Rune length or the
// Status for the special one-byte case.
xx = 0xF1 // invalid: size 1
as = 0xF0 // ASCII: size 1
s1 = 0x02 // accept 0, size 2
s2 = 0x13 // accept 1, size 3
s3 = 0x03 // accept 0, size 3
s4 = 0x23 // accept 2, size 3
s5 = 0x34 // accept 3, size 4
s6 = 0x04 // accept 0, size 4
s7 = 0x44 // accept 4, size 4
)