Merge pull request #52928 from renovate-bot/renovate/github.com-pelletier-go-toml-v2-2.x

Update module github.com/pelletier/go-toml/v2 to v2.4.3
This commit is contained in:
Sebastiaan van Stijn
2026-07-30 14:37:39 +02:00
committed by GitHub
33 changed files with 6624 additions and 5022 deletions

2
go.mod
View File

@@ -90,7 +90,7 @@ require (
github.com/opencontainers/image-spec v1.1.1
github.com/opencontainers/runtime-spec v1.3.0
github.com/opencontainers/selinux v1.15.1
github.com/pelletier/go-toml/v2 v2.3.1
github.com/pelletier/go-toml/v2 v2.4.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.24.1
github.com/rootless-containers/rootlesskit/v3 v3.0.2

4
go.sum
View File

@@ -631,8 +631,8 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwp
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=

View File

@@ -74,39 +74,43 @@ universal_binaries:
name_template: jsontoml
archives:
- id: jsontoml
format: tar.xz
builds:
formats:
- tar.xz
ids:
- jsontoml
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomljson
format: tar.xz
builds:
formats:
- tar.xz
ids:
- tomljson
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomll
format: tar.xz
builds:
formats:
- tar.xz
ids:
- tomll
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
dockers:
dockers_v2:
- 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
images:
- "ghcr.io/pelletier/go-toml"
tags:
- "latest"
- "{{ .Tag }}"
- "v{{ .Major }}"
platforms:
- linux/amd64
checksum:
name_template: 'sha256sums.txt'
snapshot:

View File

@@ -53,6 +53,14 @@ go-toml is a TOML library for Go. The goal is to provide an easy-to-use and effi
- Commit messages must explain **why** the change is needed
- Keep messages clear and informative even if details are in the PR description
### Capabilities
go-toml tracks system-level capabilities using [capslock](https://github.com/google/capslock). The baseline is in `capability_baseline.txt` and CI enforces that it does not grow.
- **Do not introduce new capabilities.** PRs that increase the capability set (e.g., adding network access, subprocess execution, syscalls) are unlikely to be accepted.
- If a change causes the capabilities check to fail, do not update the baseline to make it pass. Instead, rethink the approach to avoid requiring new capabilities.
- To check locally: `./caps.sh check` (requires `capslock` installed via `go install github.com/google/capslock/cmd/capslock@latest`)
## Pull Request Checklist
Before submitting:
@@ -61,4 +69,5 @@ Before submitting:
2. No backward-incompatible changes (unless discussed)
3. Relevant documentation added/updated
4. No performance regression (verify with benchmarks)
5. Title is clear and understandable for changelog
5. Capabilities are not increasing (`./caps.sh check`)
6. Title is clear and understandable for changelog

View File

@@ -180,6 +180,25 @@ description. Pull requests that lower performance will receive more scrutiny.
[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
### Capabilities
We use [capslock](https://github.com/google/capslock) to track what
system-level capabilities (file access, network, syscalls, etc.) each package
requires. The current baseline is in `capability_baseline.txt`. CI will fail if
a change introduces a new capability.
**Pull requests that increase the set of capabilities are unlikely to be
accepted.** go-toml is a parsing library and should not need network access,
subprocess execution, or other capabilities beyond what it already uses.
If you believe a new capability is genuinely needed, discuss it in an issue
first. To update the baseline after approval:
```bash
go install github.com/google/capslock/cmd/capslock@latest
./caps.sh generate
```
### Style
Try to look around and follow the same format and structure as the rest of the

View File

@@ -1,5 +1,6 @@
FROM scratch
ENV PATH "$PATH:/bin"
COPY tomll /bin/tomll
COPY tomljson /bin/tomljson
COPY jsontoml /bin/jsontoml
ARG TARGETPLATFORM
COPY $TARGETPLATFORM/tomll /bin/tomll
COPY $TARGETPLATFORM/tomljson /bin/tomljson
COPY $TARGETPLATFORM/jsontoml /bin/jsontoml

View File

@@ -2,7 +2,7 @@
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.1.0](https://toml.io/en/v1.1.0).
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
@@ -28,6 +28,11 @@ import "github.com/pelletier/go-toml/v2"
As much as possible, this library is designed to behave similarly as the
standard library's `encoding/json`.
When encoding structs, fields tagged with `omitempty` are omitted if they are
empty. For `time.Time`, the zero value is considered empty, so timestamps such
as `created_at` or `updated_at` are not written unless you remove `omitempty`
from the struct tag or use a pointer type (`*time.Time`).
### Performance
While go-toml favors usability, it is written with performance in mind. Most
@@ -65,7 +70,7 @@ this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
making them convenient yet unambiguous structures for their respective TOML
representation.
[ldt]: https://toml.io/en/v1.0.0#local-date-time
[ldt]: https://toml.io/en/v1.1.0#local-date-time
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
@@ -237,12 +242,12 @@ Execution time speedup compared to other Go TOML libraries:
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>2.1x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>2.0x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.3x</td><td>2.5x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>3.3x</td><td>2.8x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.9x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.0x</td></tr>
<tr><td>Marshal/HugoFrontMatter-2</td><td>2.3x</td><td>2.4x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>2.2x</td><td>2.6x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>4.9x</td><td>5.0x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>7.8x</td><td>5.9x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>6.8x</td><td>6.4x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>6.8x</td><td>6.3x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
@@ -255,17 +260,17 @@ provided for completeness.</p>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>2.0x</td><td>2.9x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>3.6x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.2x</td><td>3.4x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.9x</td><td>4.4x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.2x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.4x</td><td>2.8x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.7x</td><td>2.5x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.3x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.9x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>5.4x</td><td>3.0x</td></tr>
<tr><td>geomean</td><td>2.9x</td><td>2.8x</td></tr>
<tr><td>Marshal/SimpleDocument/map-2</td><td>2.1x</td><td>3.1x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>3.4x</td><td>4.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>10.1x</td><td>7.0x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>12.4x</td><td>8.0x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>8.2x</td><td>6.9x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>7.5x</td><td>8.3x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>9.0x</td><td>7.6x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>5.0x</td><td>4.5x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>6.4x</td><td>4.7x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>10.2x</td><td>6.1x</td></tr>
<tr><td>geomean</td><td>5.8x</td><td>5.3x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
@@ -309,304 +314,6 @@ Multiple versions are available on [ghcr.io][docker].
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
## Migrating from v1
This section describes the differences between v1 and v2, with some pointers on
how to get the original behavior when possible.
### Decoding / Unmarshal
#### Automatic field name guessing
When unmarshaling to a struct, if a key in the TOML document does not exactly
match the name of a struct field or any of the `toml`-tagged field, v1 tries
multiple variations of the key ([code][v1-keys]).
V2 instead does a case-insensitive matching, like `encoding/json`.
This could impact you if you are relying on casing to differentiate two fields,
and one of them is a not using the `toml` struct tag. The recommended solution
is to be specific about tag names for those fields using the `toml` struct tag.
[v1-keys]: https://github.com/pelletier/go-toml/blob/a2e52561804c6cd9392ebf0048ca64fe4af67a43/marshal.go#L775-L781
#### Ignore preexisting value in interface
When decoding into a non-nil `interface{}`, go-toml v1 uses the type of the
element in the interface to decode the object. For example:
```go
type inner struct {
B interface{}
}
type doc struct {
A interface{}
}
d := doc{
A: inner{
B: "Before",
},
}
data := `
[A]
B = "After"
`
toml.Unmarshal([]byte(data), &d)
fmt.Printf("toml v1: %#v\n", d)
// toml v1: main.doc{A:main.inner{B:"After"}}
```
In this case, field `A` is of type `interface{}`, containing a `inner` struct.
V1 sees that type and uses it when decoding the object.
When decoding an object into an `interface{}`, V2 instead disregards whatever
value the `interface{}` may contain and replaces it with a
`map[string]interface{}`. With the same data structure as above, here is what
the result looks like:
```go
toml.Unmarshal([]byte(data), &d)
fmt.Printf("toml v2: %#v\n", d)
// toml v2: main.doc{A:map[string]interface {}{"B":"After"}}
```
This is to match `encoding/json`'s behavior. There is no way to make the v2
decoder behave like v1.
#### Values out of array bounds ignored
When decoding into an array, v1 returns an error when the number of elements
contained in the doc is superior to the capacity of the array. For example:
```go
type doc struct {
A [2]string
}
d := doc{}
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
fmt.Println(err)
// (1, 1): unmarshal: TOML array length (3) exceeds destination array length (2)
```
In the same situation, v2 ignores the last value:
```go
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
fmt.Println("err:", err, "d:", d)
// err: <nil> d: {[one two]}
```
This is to match `encoding/json`'s behavior. There is no way to make the v2
decoder behave like v1.
#### Support for `toml.Unmarshaler` has been dropped
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`
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
#### Default struct fields order
V1 emits struct fields order alphabetically by default. V2 struct fields are
emitted in order they are defined. For example:
```go
type S struct {
B string
A string
}
data := S{
B: "B",
A: "A",
}
b, _ := tomlv1.Marshal(data)
fmt.Println("v1:\n" + string(b))
b, _ = tomlv2.Marshal(data)
fmt.Println("v2:\n" + string(b))
// Output:
// v1:
// A = "A"
// B = "B"
// v2:
// B = 'B'
// A = 'A'
```
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, or generate
struct types using `reflect.StructOf`.
#### No indentation by default
V1 automatically indents content of tables by default. V2 does not. However the
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
```go
data := map[string]interface{}{
"table": map[string]string{
"key": "value",
},
}
b, _ := tomlv1.Marshal(data)
fmt.Println("v1:\n" + string(b))
b, _ = tomlv2.Marshal(data)
fmt.Println("v2:\n" + string(b))
buf := bytes.Buffer{}
enc := tomlv2.NewEncoder(&buf)
enc.SetIndentTables(true)
enc.Encode(data)
fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
// Output:
// v1:
//
// [table]
// key = "value"
//
// v2:
// [table]
// key = 'value'
//
//
// v2 Encoder:
// [table]
// key = 'value'
```
[sit]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Encoder.SetIndentTables
#### Keys and strings are single quoted
V1 always uses double quotes (`"`) around strings and keys that cannot be
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
unless a character cannot be represented, then falls back to double quotes. 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.
#### `TextMarshaler` emits as a string, not TOML
Types that implement [`encoding.TextMarshaler`][tm] can emit arbitrary TOML in
v1. The encoder would append the result to the output directly. In v2 the result
is wrapped in a string. As a result, this interface cannot be implemented by the
root object.
There is no way to make v2 encoder behave like v1.
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
#### `Encoder.CompactComments` has been removed
Emitting compact comments is now the default behavior of go-toml. This option
is not necessary anymore.
#### Struct tags have been merged
V1 used to provide multiple struct tags: `comment`, `commented`, `multiline`,
`toml`, and `omitempty`. To behave more like the standard library, v2 has merged
`toml`, `multiline`, `commented`, and `omitempty`. For example:
```go
type doc struct {
// v1
F string `toml:"field" multiline:"true" omitempty:"true" commented:"true"`
// v2
F string `toml:"field,multiline,omitempty,commented"`
}
```
Has a result, the `Encoder.SetTag*` methods have been removed, as there is just
one tag now.
#### `Encoder.ArraysWithOneElementPerLine` has been renamed
The new name is `Encoder.SetArraysMultiline`. The behavior should be the same.
#### `Encoder.Indentation` has been renamed
The new name is `Encoder.SetIndentSymbol`. The behavior should be the same.
#### Embedded structs behave like stdlib
V1 defaults to merging embedded struct fields into the embedding struct. This
behavior was unexpected because it does not follow the standard library. To
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
added to make the encoder behave correctly. Given backward compatibility is not
a problem anymore, v2 does the right thing by default: it follows the behavior
of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.
[nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038
### `query`
go-toml v1 provided the [`go-toml/query`][query] package. It allowed to run
JSONPath-style queries on TOML files. This feature is not available in v2. For a
replacement, check out [dasel][dasel].
This package has been removed because it was essentially not supported anymore
(last commit May 2020), increased the complexity of the code base, and more
complete solutions exist out there.
[query]: https://github.com/pelletier/go-toml/tree/f99d6bbca119636aeafcf351ee52b3d202782627/query
[dasel]: https://github.com/TomWright/dasel
## Versioning
Expect for parts explicitly marked otherwise, go-toml follows [Semantic

View File

@@ -0,0 +1 @@
github.com/pelletier/go-toml/v2: CAPABILITY_REFLECT, CAPABILITY_UNANALYZED, CAPABILITY_UNSAFE_POINTER

101
vendor/github.com/pelletier/go-toml/v2/caps.sh generated vendored Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
#
# Generates or checks the capability baseline for go-toml.
#
# Usage:
# ./caps.sh generate # regenerate capability_baseline.txt
# ./caps.sh check # check that capabilities haven't grown
#
# Requires: go, capslock (go install github.com/google/capslock/cmd/capslock@latest)
set -euo pipefail
BASELINE="capability_baseline.txt"
CAPSLOCK="${CAPSLOCK:-capslock}"
# Capabilities that must never appear in any package.
FORBIDDEN_CAPS=(
CAPABILITY_NETWORK
CAPABILITY_CGO
CAPABILITY_EXEC
)
capslock_to_baseline() {
"$CAPSLOCK" -packages=. -output=package -granularity=package \
| jq -r 'to_entries | sort_by(.key) | .[] | .key + ": " + (.value | sort | join(", "))'
}
generate() {
capslock_to_baseline > "$BASELINE"
echo "Wrote $BASELINE"
}
check() {
if [ ! -f "$BASELINE" ]; then
echo "ERROR: $BASELINE not found. Run '$0 generate' first."
exit 1
fi
current=$(mktemp)
trap 'rm -f "$current"' EXIT
capslock_to_baseline > "$current"
failed=0
# Check for forbidden capabilities in current output.
for cap in "${FORBIDDEN_CAPS[@]}"; do
if grep -q "$cap" "$current"; then
echo "FORBIDDEN capability found: $cap"
grep "$cap" "$current"
failed=1
fi
done
# Extract all unique capability names from baseline and current.
baseline_caps=$(grep -oE 'CAPABILITY_[A-Z_]+' "$BASELINE" | sort -u)
current_caps=$(grep -oE 'CAPABILITY_[A-Z_]+' "$current" | sort -u)
# Check for new capability names not in the baseline.
new_caps=$(comm -13 <(echo "$baseline_caps") <(echo "$current_caps"))
if [ -n "$new_caps" ]; then
echo "NEW capabilities detected (not in baseline):"
echo "$new_caps"
failed=1
fi
# Check for new per-package capabilities (a package gained a capability it didn't have before).
while IFS=': ' read -r pkg caps; do
baseline_pkg_caps=$(grep "^${pkg}:" "$BASELINE" 2>/dev/null | sed 's/^[^:]*: //' || true)
if [ -z "$baseline_pkg_caps" ]; then
echo "NEW package with capabilities: $pkg: $caps"
failed=1
continue
fi
# Check each capability in current for this package
for cap in $(echo "$caps" | tr ', ' '\n' | grep -v '^$'); do
if ! echo "$baseline_pkg_caps" | grep -q "$cap"; then
echo "NEW capability for $pkg: $cap"
failed=1
fi
done
done < "$current"
if [ "$failed" -eq 1 ]; then
echo ""
echo "FAILED: capabilities have grown."
echo "If this is intentional, run '$0 generate' and commit the updated $BASELINE."
exit 1
fi
echo "OK: no new capabilities detected."
}
case "${1:-}" in
generate) generate ;;
check) check ;;
*)
echo "Usage: $0 {generate|check}"
exit 1
;;
esac

View File

@@ -146,13 +146,17 @@ bench() {
pushd "$dir"
tags=""
if [ "${replace}" != "" ]; then
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2\"|${replace}\"|g" {} \;
go get "${replace}"
# The realworld benchmarks use v2-only API and cannot compile against
# the other libraries; exclude them from cross-library comparisons.
tags="-tags cross_library_benchmark"
fi
export GOMAXPROCS=2
go test '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}"
go test ${tags} '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}"
popd
if [ "${branch}" != "HEAD" ]; then

View File

@@ -1,6 +1,7 @@
package toml
import (
"bytes"
"fmt"
"math"
"strconv"
@@ -22,15 +23,259 @@ func parseInteger(b []byte) (int64, error) {
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
}
}
return parseIntDec(b)
}
func parseIntHex(b []byte) (int64, error) {
var v uint64
for _, c := range b[2:] {
if c == '_' {
continue
}
var d byte
switch {
case c >= '0' && c <= '9':
d = c - '0'
case c >= 'a' && c <= 'f':
d = c - 'a' + 10
case c >= 'A' && c <= 'F':
d = c - 'A' + 10
}
if v > math.MaxInt64>>4 {
return 0, unstable.NewParserError(b, "hexadecimal number is too large to fit in a 64-bit signed integer")
}
v = v<<4 | uint64(d)
}
return int64(v), nil
}
func parseIntOct(b []byte) (int64, error) {
var v uint64
for _, c := range b[2:] {
if c == '_' {
continue
}
if v > math.MaxInt64>>3 {
return 0, unstable.NewParserError(b, "octal number is too large to fit in a 64-bit signed integer")
}
v = v<<3 | uint64(c-'0')
}
return int64(v), nil
}
func parseIntBin(b []byte) (int64, error) {
var v uint64
for _, c := range b[2:] {
if c == '_' {
continue
}
if v > math.MaxInt64>>1 {
return 0, unstable.NewParserError(b, "binary number is too large to fit in a 64-bit signed integer")
}
v = v<<1 | uint64(c-'0')
}
return int64(v), nil
}
func parseIntDec(b []byte) (int64, error) {
i := 0
neg := false
switch b[0] {
case '-':
neg = true
i++
case '+':
i++
}
var limit uint64 = math.MaxInt64
if neg {
limit = math.MaxInt64 + 1
}
var v uint64
for ; i < len(b); i++ {
c := b[i]
if c == '_' {
continue
}
if v > limit/10 {
return 0, unstable.NewParserError(b, "decimal number is too large to fit in a 64-bit signed integer")
}
v = v*10 + uint64(c-'0')
if v > limit {
return 0, unstable.NewParserError(b, "decimal number is too large to fit in a 64-bit signed integer")
}
}
if neg {
return -int64(v), nil //nolint:gosec // v <= MaxInt64+1, the conversion wraps to the intended negative value
}
return int64(v), nil //nolint:gosec // v <= MaxInt64
}
func parseFloat(b []byte) (float64, error) {
i := 0
if len(b) > 0 && (b[0] == '+' || b[0] == '-') {
i = 1
}
if len(b) == i+3 {
switch b[i] {
case 'i':
// inf
if b[0] == '-' {
return math.Inf(-1), nil
}
return math.Inf(1), nil
case 'n':
// nan
return math.NaN(), nil
}
}
// Fast path: a plain decimal whose significand fits in 53 bits and whose
// base-10 exponent is within [-22, 22] is parsed exactly with a single
// rounding (Clinger's method) straight from the bytes, with no string
// allocation and no full strconv parse. This is the common shape for
// numeric data (e.g. coordinate lists). Anything outside those bounds, or
// with underscores, falls through to strconv, which is the reference.
if f, ok := fastParseFloat(b); ok {
return f, nil
}
// strconv.ParseFloat is the reference implementation for parsing
// floating point numbers. The position of underscores has already been
// validated by the parser; strip them so that they do not interfere with
// Go's own underscore rules.
cleaned := b
if bytes.IndexByte(b, '_') >= 0 {
cleaned = make([]byte, 0, len(b))
for _, c := range b {
if c != '_' {
cleaned = append(cleaned, c)
}
}
}
f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil {
return 0, unstable.NewParserError(b, "unable to parse float: %s", err)
}
return f, nil
}
// float64pow10 holds the powers of ten that are exactly representable as a
// float64 (10^0 .. 10^22).
var float64pow10 = [...]float64{
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
}
// fastParseFloat parses b as a float64 using Clinger's exact method and reports
// whether it applied. It accepts only plain decimal numbers (optional sign,
// digits, one optional '.', optional 'e'/'E' exponent) whose significand fits
// in 53 bits and whose effective base-10 exponent is within [-22, 22]; under
// those conditions float64(significand) * 10^exp (or / 10^-exp) is the exact,
// correctly-rounded result, identical to strconv.ParseFloat. It returns
// ok=false (deferring to strconv) for underscores, hexadecimal floats, large
// significands or exponents, and any other shape.
func fastParseFloat(b []byte) (float64, bool) {
i := 0
neg := false
if i < len(b) && (b[i] == '+' || b[i] == '-') {
neg = b[i] == '-'
i++
}
var mantissa uint64
digits := 0
fracDigits := 0
sawDot := false
sawDigit := false
for ; i < len(b); i++ {
c := b[i]
switch {
case c >= '0' && c <= '9':
if digits >= 19 {
// Too many significant digits to accumulate without risking a
// uint64 overflow (and well past the 53-bit exact range).
return 0, false
}
mantissa = mantissa*10 + uint64(c-'0')
digits++
if sawDot {
fracDigits++
}
sawDigit = true
case c == '.':
if sawDot {
return 0, false
}
sawDot = true
default:
goto exponent
}
}
exponent:
if !sawDigit {
return 0, false
}
exp := -fracDigits
if i < len(b) && (b[i] == 'e' || b[i] == 'E') {
i++
esign := 1
if i < len(b) && (b[i] == '+' || b[i] == '-') {
if b[i] == '-' {
esign = -1
}
i++
}
if i >= len(b) {
return 0, false
}
eval := 0
for ; i < len(b); i++ {
c := b[i]
if c < '0' || c > '9' {
return 0, false
}
eval = eval*10 + int(c-'0')
if eval > 1000 {
return 0, false
}
}
exp += esign * eval
}
if i != len(b) {
// Trailing bytes (an underscore, a hexadecimal marker, ...).
return 0, false
}
if mantissa > 1<<53 {
return 0, false
}
f := float64(mantissa)
switch {
case exp == 0:
case exp > 0 && exp <= 22:
f *= float64pow10[exp]
case exp < 0 && exp >= -22:
f /= float64pow10[-exp]
default:
return 0, false
}
if neg {
f = -f
}
return f, true
}
func isDecimalDigit(c byte) bool {
return c >= '0' && c <= '9'
}
// parseLocalDate parses a date of the exact form YYYY-MM-DD and validates
// its components.
func parseLocalDate(b []byte) (LocalDate, error) {
// full-date = date-fullyear "-" date-month "-" date-mday
// date-fullyear = 4DIGIT
// date-month = 2DIGIT ; 01-12
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
@@ -38,49 +283,184 @@ func parseLocalDate(b []byte) (LocalDate, error) {
}
var err error
date.Year, err = parseDecimalDigits(b[0:4])
if err != nil {
return LocalDate{}, err
return date, err
}
date.Month, err = parseDecimalDigits(b[5:7])
if err != nil {
return LocalDate{}, err
return date, err
}
date.Day, err = parseDecimalDigits(b[8:10])
if err != nil {
return LocalDate{}, err
return date, err
}
if !isValidDate(date.Year, date.Month, date.Day) {
return LocalDate{}, unstable.NewParserError(b, "impossible date")
if date.Month < 1 || date.Month > 12 {
return date, unstable.NewParserError(b[5:7], "impossible date")
}
maxDay := daysIn(date.Month, date.Year)
if date.Day < 1 || date.Day > maxDay {
return date, unstable.NewParserError(b[8:10], "impossible date")
}
return date, nil
}
func daysIn(month int, year int) int {
switch month {
case 2:
if isLeapYear(year) {
return 29
}
return 28
case 4, 6, 9, 11:
return 30
default:
return 31
}
}
func isLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
// parseDecimalDigits parses a sequence of digits as a decimal number.
func parseDecimalDigits(b []byte) (int, error) {
v := 0
for i, c := range b {
if c < '0' || c > '9' {
if !isDecimalDigit(c) {
return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)")
}
v *= 10
v += int(c - '0')
v = v*10 + int(c-'0')
}
return v, nil
}
func parseDateTime(b []byte) (time.Time, error) {
// offset-date-time = full-date time-delim full-time
// full-time = partial-time time-offset
// time-offset = "Z" / time-numoffset
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
// parseLocalTime parses a time of the form HH:MM with optional seconds and an
// optional fractional part (TOML v1.1.0). It returns the remaining bytes after
// the time.
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
var (
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
t LocalTime
)
// check if b matches to have expected format HH:MM[:SS[.NNNNNN]]
const localTimeByteMinLen = 5
if len(b) < localTimeByteMinLen {
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM[:SS[.NNNNNN]]")
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
}
if t.Hour > 23 {
return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
}
if b[2] != ':' {
return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes")
}
t.Minute, err = parseDecimalDigits(b[3:5])
if err != nil {
return t, nil, err
}
if t.Minute > 59 {
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
}
b = b[5:]
// Seconds are optional (TOML v1.1.0). Fractional seconds may only appear
// when seconds are present:
// partial-time = time-hour ":" time-minute [ ":" time-second [ time-secfrac ] ]
secondsPresent := false
if len(b) >= 1 && b[0] == ':' {
if len(b) < 3 {
return t, nil, unstable.NewParserError(b, "incomplete seconds")
}
t.Second, err = parseDecimalDigits(b[1:3])
if err != nil {
return t, nil, err
}
if t.Second > 59 {
return t, nil, unstable.NewParserError(b[1:3], "seconds cannot be greater than 59")
}
b = b[3:]
secondsPresent = true
}
if secondsPresent && len(b) >= 1 && b[0] == '.' {
frac := 0
precision := 0
digits := 0
for i, c := range b[1:] {
if !isDecimalDigit(c) {
if i == 0 {
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
}
break
}
digits++
if i < 9 {
frac = frac*10 + int(c-'0')
precision++
}
}
if digits == 0 {
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
}
t.Nanosecond = frac * nspow[precision]
t.Precision = precision
return t, b[1+digits:], nil
}
return t, b, nil
}
// parseLocalDateTime parses a local date time of the form
// YYYY-MM-DD(T| )HH:MM:SS[.NNNNNN]. It returns the remaining bytes after the
// date-time.
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
var dt LocalDateTime
const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen {
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM[:SS[.NNNNNNNNN]]")
}
date, err := parseLocalDate(b[:10])
if err != nil {
return dt, nil, err
}
dt.LocalDate = date
sep := b[10]
if sep != 'T' && sep != ' ' && sep != 't' {
return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space")
}
t, rest, err := parseLocalTime(b[11:])
if err != nil {
return dt, nil, err
}
dt.LocalTime = t
return dt, rest, nil
}
// parseDateTime parses a date-time with a timezone offset (Z or +/-HH:MM).
func parseDateTime(b []byte) (time.Time, error) {
dt, b, err := parseLocalDateTime(b)
if err != nil {
return time.Time{}, err
@@ -89,8 +469,8 @@ func parseDateTime(b []byte) (time.Time, error) {
var zone *time.Location
if len(b) == 0 {
// parser should have checked that when assigning the date time node
panic("date time should have a timezone")
// parser should have checked that there is a timezone
return time.Time{}, unstable.NewParserError(b, "date-time is missing timezone")
}
if b[0] == 'Z' || b[0] == 'z' {
@@ -120,7 +500,7 @@ func parseDateTime(b []byte) (time.Time, error) {
return time.Time{}, err
}
if hours > 23 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset hours")
return time.Time{}, unstable.NewParserError(b[1:3], "invalid timezone offset hours")
}
minutes, err := parseDecimalDigits(b[4:6])
@@ -128,7 +508,7 @@ func parseDateTime(b []byte) (time.Time, error) {
return time.Time{}, err
}
if minutes > 59 {
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset minutes")
return time.Time{}, unstable.NewParserError(b[4:6], "invalid timezone offset minutes")
}
seconds := direction * (hours*3600 + minutes*60)
@@ -156,394 +536,3 @@ func parseDateTime(b []byte) (time.Time, error) {
return t, nil
}
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
var dt LocalDateTime
const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen {
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
}
date, err := parseLocalDate(b[:10])
if err != nil {
return dt, nil, err
}
dt.LocalDate = date
sep := b[10]
if sep != 'T' && sep != ' ' && sep != 't' {
return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space")
}
t, rest, err := parseLocalTime(b[11:])
if err != nil {
return dt, nil, err
}
dt.LocalTime = t
return dt, rest, nil
}
// parseLocalTime is a bit different because it also returns the remaining
// []byte that is didn't need. This is to allow parseDateTime to parse those
// remaining bytes as a timezone.
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
var (
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
t LocalTime
)
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
const localTimeByteLen = 8
if len(b) < localTimeByteLen {
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
}
if t.Hour > 23 {
return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
}
if b[2] != ':' {
return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes")
}
t.Minute, err = parseDecimalDigits(b[3:5])
if err != nil {
return t, nil, err
}
if t.Minute > 59 {
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
}
if b[5] != ':' {
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
}
t.Second, err = parseDecimalDigits(b[6:8])
if err != nil {
return t, nil, err
}
if t.Second > 59 {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
}
b = b[8:]
if len(b) >= 1 && b[0] == '.' {
frac := 0
precision := 0
digits := 0
for i, c := range b[1:] {
if !isDigit(c) {
if i == 0 {
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
}
break
}
digits++
const maxFracPrecision = 9
if i >= maxFracPrecision {
// go-toml allows decoding fractional seconds
// beyond the supported precision of 9
// digits. It truncates the fractional component
// to the supported precision and ignores the
// remaining digits.
//
// https://github.com/pelletier/go-toml/discussions/707
continue
}
frac *= 10
frac += int(c - '0')
precision++
}
if precision == 0 {
return t, nil, unstable.NewParserError(b[:1], "nanoseconds need at least one digit")
}
t.Nanosecond = frac * nspow[precision]
t.Precision = precision
return t, b[1+digits:], nil
}
return t, b, nil
}
func parseFloat(b []byte) (float64, error) {
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil
}
cleaned, err := checkAndRemoveUnderscoresFloats(b)
if err != nil {
return 0, err
}
if cleaned[0] == '.' {
return 0, unstable.NewParserError(b, "float cannot start with a dot")
}
if cleaned[len(cleaned)-1] == '.' {
return 0, unstable.NewParserError(b, "float cannot end with a dot")
}
dotAlreadySeen := false
for i, c := range cleaned {
if c == '.' {
if dotAlreadySeen {
return 0, unstable.NewParserError(b[i:i+1], "float can have at most one decimal point")
}
if !isDigit(cleaned[i-1]) {
return 0, unstable.NewParserError(b[i-1:i+1], "float decimal point must be preceded by a digit")
}
if !isDigit(cleaned[i+1]) {
return 0, unstable.NewParserError(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' && len(cleaned) > start+1 && isDigit(cleaned[start+1]) {
return 0, unstable.NewParserError(b, "float integer part cannot have leading zeroes")
}
f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil {
return 0, unstable.NewParserError(b, "unable to parse float: %w", err)
}
return f, nil
}
func parseIntHex(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 16, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse hexadecimal number: %w", err)
}
return i, nil
}
func parseIntOct(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 8, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse octal number: %w", err)
}
return i, nil
}
func parseIntBin(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 2, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse binary number: %w", err)
}
return i, nil
}
func isSign(b byte) bool {
return b == '+' || b == '-'
}
func parseIntDec(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
if err != nil {
return 0, err
}
startIdx := 0
if isSign(cleaned[0]) {
startIdx++
}
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
return 0, unstable.NewParserError(b, "leading zero not allowed on decimal number")
}
i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil {
return 0, unstable.NewParserError(b, "couldn't parse decimal number: %w", err)
}
return i, nil
}
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
start := 0
if b[start] == '+' || b[start] == '-' {
start++
}
if len(b) == start {
return b, nil
}
if b[start] == '_' {
return nil, unstable.NewParserError(b[start:start+1], "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, unstable.NewParserError(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, i, len(b))
copy(cleaned, b)
for i++; i < len(b); i++ {
c := b[i]
if c == '_' {
if !before {
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
}
before = false
} else {
before = true
cleaned = append(cleaned, c)
}
}
return cleaned, nil
}
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
if b[0] == '_' {
return nil, unstable.NewParserError(b[0:1], "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, unstable.NewParserError(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, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
}
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore before exponent")
}
before = false
case '+', '-':
// signed exponents
cleaned = append(cleaned, c)
before = false
case 'e', 'E':
if i < len(b)-1 && b[i+1] == '_' {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after exponent")
}
cleaned = append(cleaned, c)
case '.':
if i < len(b)-1 && b[i+1] == '_' {
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after decimal point")
}
if i > 0 && b[i-1] == '_' {
return nil, unstable.NewParserError(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)
}
func isDigit(r byte) bool {
return r >= '0' && r <= '9'
}

362
vendor/github.com/pelletier/go-toml/v2/decode_fused.go generated vendored Normal file
View File

@@ -0,0 +1,362 @@
package toml
import (
"errors"
"reflect"
"strings"
"github.com/pelletier/go-toml/v2/internal/parserbridge"
"github.com/pelletier/go-toml/v2/unstable"
)
// unmarshalFused decodes a whole document into a native map[string]interface{}
// tree with no reflection on the document structure, and without building an
// AST for table headers and scalar key-values. Only container values (arrays
// and inline tables) are parsed into the parser arena, so that the seen-tracker
// can validate them and decodeAny can presize the resulting slices and maps —
// the AST is what makes that cheap O(1) presizing possible.
//
// It is used when the target is a fully generic value (interface{} or
// map[string]interface{}) and the unmarshaler interface is disabled. The
// seen-tracker validates the document (duplicate keys, type consistency), so
// the builder creates and merges containers without revalidating. Strict mode
// never applies to a generic target (a map has no "unknown fields"), and
// captures never apply (a generic value implements no Unmarshaler).
func (d *decoder) unmarshalFused(root reflect.Value, data []byte) error {
var m map[string]interface{}
if !root.IsNil() {
// Decode into (merge with) an existing generic map when present.
if em, ok := root.Interface().(map[string]interface{}); ok {
m = em
}
}
if m == nil {
m = map[string]interface{}{}
}
if err := d.fusedDocument(m, data); err != nil {
return d.wrapFusedError(data, err)
}
if root.CanSet() {
root.Set(reflect.ValueOf(m))
}
return nil
}
// fusedDocument runs the top-level expression loop, mirroring
// Parser.NextExpression but storing values directly into native maps.
func (d *decoder) fusedDocument(m map[string]interface{}, b []byte) error {
cur := m
for {
b = fusedSkipWS(b)
if len(b) == 0 {
return nil
}
switch b[0] {
case '\n':
b = b[1:]
case '\r':
if len(b) > 1 && b[1] == '\n' {
b = b[2:]
continue
}
return unstable.NewParserError(b[:1], "expected newline but got %#U", b[0])
case '#':
_, rest, err := parserbridge.ScanComment(b)
if err != nil {
return err
}
rest, err = fusedConsumeEOL(rest)
if err != nil {
return err
}
b = rest
case '[':
rest, err := d.fusedTable(b, m, &cur)
if err != nil {
return err
}
b = rest
default:
rest, err := d.fusedKeyVal(b, cur)
if err != nil {
return err
}
b = rest
}
}
}
// fusedTable handles a [table] or [[array table]] header. b starts at '['. It
// updates *cur to the table the following key-values belong to.
func (d *decoder) fusedTable(b []byte, root map[string]interface{}, cur *map[string]interface{}) ([]byte, error) {
arrayTable := len(b) > 1 && b[1] == '['
var start []byte
if arrayTable {
start = fusedSkipWS(b[2:])
} else {
start = fusedSkipWS(b[1:])
}
var err error
var rawKey []byte
d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, start, d.keyParts[:0])
if err != nil {
return nil, err
}
if arrayTable {
if len(b) < 2 || b[0] != ']' || b[1] != ']' {
return nil, unstable.NewParserError(fusedHL1(b), "expected ']]' to close array table name")
}
b = b[2:]
} else {
if len(b) == 0 || b[0] != ']' {
return nil, unstable.NewParserError(fusedHL1(b), "expected ']' to close table name")
}
b = b[1:]
}
// The whole expression (including its line termination) is parsed before
// it is validated, to keep error precedence identical to the AST path.
b, err = d.fusedFinishLine(b)
if err != nil {
return nil, err
}
if arrayTable {
first, err := d.seen.CheckArrayTable(d.keyParts)
if err != nil {
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
}
*cur = d.anyArrayTableParts(root, d.keyParts, first)
} else {
if _, err := d.seen.CheckTable(d.keyParts); err != nil {
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
}
*cur = d.anyTableParts(root, d.keyParts)
}
return b, nil
}
// fusedKeyVal handles a `key = value` expression relative to the current table
// cur. b starts at the first character of the key.
func (d *decoder) fusedKeyVal(b []byte, cur map[string]interface{}) ([]byte, error) {
var err error
var rawKey []byte
d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, b, d.keyParts[:0])
if err != nil {
return nil, err
}
if len(b) == 0 || b[0] != '=' {
return nil, unstable.NewParserError(fusedHL1(b), "expected '=' after key")
}
b = fusedSkipWS(b[1:])
if len(b) == 0 {
return nil, unstable.NewParserError(b, "expected value, not end of input")
}
if c := b[0]; c == '[' || c == '{' {
// Container value: build its AST so the seen-tracker can validate it
// and decodeAny can presize the resulting slices and maps.
nodeAny, rest, err := parserbridge.ParseValue(&d.p, b)
if err != nil {
return nil, err
}
node := nodeAny.(*unstable.Node)
rest, err = d.fusedFinishLine(rest)
if err != nil {
return nil, err
}
leafID, err := d.seen.CheckKeyValue(d.keyParts)
if err != nil {
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
}
if err := d.seen.CheckValueUnder(leafID, node); err != nil {
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
}
av, err := d.decodeAny(node)
if err != nil {
return nil, err
}
d.setFusedLeaf(cur, d.keyParts, av)
return rest, nil
}
// Scalar value: scan it without building a node, then validate and convert
// it natively.
k, _, value, rest, err := parserbridge.ScanScalar(&d.p, b)
if err != nil {
return nil, err
}
kind := unstable.Kind(k)
rest, err = d.fusedFinishLine(rest)
if err != nil {
return nil, err
}
if _, err := d.seen.CheckKeyValue(d.keyParts); err != nil {
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
}
av, err := d.fusedScalar(kind, value)
if err != nil {
return nil, err
}
d.setFusedLeaf(cur, d.keyParts, av)
return rest, nil
}
// fusedSeenError turns a bare error returned by a SeenTracker parts-method
// into a ParserError carrying the position (the raw key span) and key path of
// the offending expression, so that it is reported as a DecodeError with
// context. It mirrors decoder.wrapSeenError for the fused (AST-less) path.
func (d *decoder) fusedSeenError(rawKey []byte, parts [][]byte, err error) error {
key := make(Key, len(parts))
for i, p := range parts {
key[i] = string(p)
}
return &unstable.ParserError{
Highlight: rawKey,
Message: strings.TrimPrefix(err.Error(), "toml: "),
Key: key,
}
}
// fusedScalar converts a scanned scalar value into the native Go value used
// for generic targets. It mirrors the scalar cases of decodeAny.
func (d *decoder) fusedScalar(kind unstable.Kind, value []byte) (interface{}, error) {
switch kind {
case unstable.String:
return string(value), nil
case unstable.Integer:
i, err := parseInteger(value)
return i, err
case unstable.Float:
f, err := parseFloat(value)
return f, err
case unstable.Bool:
return value[0] == 't', nil
case unstable.DateTime:
t, err := parseDateTime(value)
return t, err
case unstable.LocalDateTime:
dt, rest, err := parseLocalDateTime(value)
if err != nil {
return nil, err
}
if len(rest) > 0 {
return nil, unstable.NewParserError(rest, "extra characters at the end of a local date time")
}
return dt, nil
case unstable.LocalDate:
date, err := parseLocalDate(value)
return date, err
case unstable.LocalTime:
t, rest, err := parseLocalTime(value)
if err != nil {
return nil, err
}
if len(rest) > 0 {
return nil, unstable.NewParserError(rest, "extra characters at the end of a local time")
}
return t, nil
default:
return nil, unstable.NewParserError(value, "unsupported value kind %s", kind)
}
}
// anyTableParts navigates a [table] header (given its key parts) to the map it
// designates, creating intermediate tables as needed.
func (d *decoder) anyTableParts(m map[string]interface{}, parts [][]byte) map[string]interface{} {
cur := m
for _, p := range parts {
cur = d.anyChildTable(cur, d.intern(p))
}
return cur
}
// anyArrayTableParts navigates a [[array table]] header (given its key parts),
// appends a fresh element to the designated array, and returns it. first is
// true the first time this header is seen, in which case any pre-existing array
// (from a reused target) is reset.
func (d *decoder) anyArrayTableParts(m map[string]interface{}, parts [][]byte, first bool) map[string]interface{} {
cur := m
name := d.intern(parts[0])
for i := 1; i < len(parts); i++ {
cur = d.anyChildTable(cur, name)
name = d.intern(parts[i])
}
s, _ := cur[name].([]interface{})
if first {
s = s[:0]
}
elem := map[string]interface{}{}
cur[name] = append(s, elem)
return elem
}
// setFusedLeaf assigns av at the (possibly dotted) key parts within cur,
// creating intermediate maps as needed.
func (d *decoder) setFusedLeaf(cur map[string]interface{}, parts [][]byte, av interface{}) {
for i := 0; i < len(parts)-1; i++ {
cur = d.anyChildTable(cur, d.intern(parts[i]))
}
cur[d.intern(parts[len(parts)-1])] = av
}
// wrapFusedError gives document context to errors produced by the fused
// decoder.
func (d *decoder) wrapFusedError(data []byte, err error) error {
var perr *unstable.ParserError
if errors.As(err, &perr) && len(perr.Highlight) == 0 {
// Mirror NextExpression: give end-of-input errors a usable position by
// extending the empty highlight to the last byte of the document.
if offset := cap(data) - cap(perr.Highlight); offset > 0 && offset == len(data) {
perr.Highlight = data[offset-1 : offset]
}
}
return d.wrapError(data, err)
}
func fusedSkipWS(b []byte) []byte {
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
b = b[1:]
}
return b
}
func fusedConsumeEOL(b []byte) ([]byte, error) {
if len(b) == 0 {
return b, nil
}
switch b[0] {
case '\n':
return b[1:], nil
case '\r':
if len(b) > 1 && b[1] == '\n' {
return b[2:], nil
}
}
return nil, unstable.NewParserError(b[:1], "expected newline but got %#U", b[0])
}
// fusedFinishLine consumes `ws [comment] (newline|eof)` after an expression.
func (d *decoder) fusedFinishLine(b []byte) ([]byte, error) {
b = fusedSkipWS(b)
if len(b) > 0 && b[0] == '#' {
_, rest, err := parserbridge.ScanComment(b)
if err != nil {
return nil, err
}
b = rest
}
return fusedConsumeEOL(b)
}
func fusedHL1(b []byte) []byte {
if len(b) > 0 {
return b[:1]
}
return b
}

View File

@@ -1,8 +1,7 @@
package toml
import (
"fmt"
"reflect"
"errors"
"strconv"
"strings"
@@ -47,7 +46,6 @@ func (s *StrictMissingError) String() string {
if i > 0 {
buf.WriteString("\n---\n")
}
buf.WriteString(e.String())
}
@@ -73,7 +71,8 @@ func (e *DecodeError) Error() string {
return "toml: " + e.message
}
// String returns the human-readable contextualized error. This string is multi-line.
// String returns the human-readable contextualized error. This string is
// multi-line.
func (e *DecodeError) String() string {
return e.human
}
@@ -84,200 +83,151 @@ func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}
// Key that was being processed when the error occurred. The key is present only
// if this DecodeError is part of a StrictMissingError.
// Key that was being processed when the error occurred.
func (e *DecodeError) Key() Key {
return e.key
}
// wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document.
//
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//
//nolint:funlen
// wrapDecodeError creates a DecodeError from a ParserError. The highlight of
// the ParserError needs to be a subslice of the document.
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := subsliceOffset(document, de.Highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
before, after := linesOfContext(document, de.Highlight, offset, 3)
var buf strings.Builder
maxLine := errLine + len(after) - 1
lineColumnWidth := len(strconv.Itoa(maxLine))
// Write the lines of context strictly before the error.
for i := len(before) - 1; i > 0; i-- {
line := errLine - i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(before[i]) > 0 {
buf.WriteString(" ")
buf.Write(before[i])
}
buf.WriteRune('\n')
if de == nil {
return nil
}
return newDecodeError(document, de.Highlight, de.Key, de.Message)
}
// Write the document line that contains the error.
// newDecodeError creates a DecodeError pointing at the given highlight, which
// needs to be a subslice of the document.
func newDecodeError(document []byte, highlight []byte, key Key, message string) *DecodeError {
offset := subsliceOffset(document, highlight)
buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
buf.WriteString("| ")
errLineIdx, errColumn := positionAt(document, offset)
if len(before) > 0 {
buf.Write(before[0])
}
buf.Write(de.Highlight)
if len(after) > 0 {
buf.Write(after[0])
}
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("| ")
if len(before) > 0 {
buf.WriteString(strings.Repeat(" ", len(before[0])))
}
buf.WriteString(strings.Repeat("~", len(de.Highlight)))
if len(errMessage) > 0 {
buf.WriteString(" ")
buf.WriteString(errMessage)
}
// Write the lines of context strictly after the error.
for i := 1; i < len(after); i++ {
buf.WriteRune('\n')
line := errLine + i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(after[i]) > 0 {
buf.WriteString(" ")
buf.Write(after[i])
}
}
human := buildHumanContext(document, errLineIdx, errColumn, len(highlight), message)
return &DecodeError{
message: errMessage,
line: errLine,
message: message,
line: errLineIdx + 1,
column: errColumn,
key: de.Key,
human: buf.String(),
key: key,
human: human,
}
}
func formatLineNumber(line int, width int) string {
format := "%" + strconv.Itoa(width) + "d"
return fmt.Sprintf(format, line)
}
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
}
func beforeLines(document []byte, offset int, linesAround int) [][]byte {
var beforeLines [][]byte
// Walk the document backward from the highlight to find previous lines
// of context.
rest := document[:offset]
backward:
for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
switch {
case rest[o] == '\n':
// handle individual lines
beforeLines = append(beforeLines, rest[o+1:])
rest = rest[:o]
o = len(rest) - 1
case o == 0:
// add the first line only if it's non-empty
beforeLines = append(beforeLines, rest)
break backward
default:
o--
}
}
return beforeLines
}
func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
var afterLines [][]byte
// Walk the document forward from the highlight to find the following
// lines of context.
rest := document[offset+len(highlight):]
forward:
for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
switch {
case rest[o] == '\n':
// handle individual lines
afterLines = append(afterLines, rest[:o])
rest = rest[o+1:]
o = 0
case o == len(rest)-1:
// add last line only if it's non-empty
afterLines = append(afterLines, rest)
break forward
default:
o++
}
}
return afterLines
}
func positionAtEnd(b []byte) (row int, column int) {
row = 1
column = 1
for _, c := range b {
if c == '\n' {
row++
column = 1
} else {
column++
}
}
return row, column
}
// subsliceOffset returns the byte offset of subslice within data.
// subslice must share the same backing array as data.
func subsliceOffset(data []byte, subslice []byte) int {
if len(subslice) == 0 {
return 0
}
// Use reflect to get the data pointers of both slices.
// This is safe because we're only reading the pointer values for comparison.
dataPtr := reflect.ValueOf(data).Pointer()
subPtr := reflect.ValueOf(subslice).Pointer()
offset := int(subPtr - dataPtr)
if offset < 0 || offset > len(data) {
panic("subslice is not within data")
// subsliceOffset returns the offset of the subslice b within the document.
func subsliceOffset(document, b []byte) int {
// Highlights are subslices of the document, which means they share the
// same backing array, and their capacity counts the bytes between their
// start and the end of the backing array.
offset := cap(document) - cap(b)
if offset < 0 || offset+len(b) > len(document) {
panic(errors.New("highlight is not a subslice of the document"))
}
return offset
}
// positionAt returns the 0-indexed line and the 1-indexed column of the given
// offset in the document.
func positionAt(document []byte, offset int) (lineIdx int, column int) {
lineStart := 0
for i := 0; i < offset; i++ {
if document[i] == '\n' {
lineIdx++
lineStart = i + 1
}
}
return lineIdx, offset - lineStart + 1
}
// docLines splits the document into lines, removing the trailing newline
// characters.
func docLines(document []byte) []string {
s := string(document)
lines := strings.Split(s, "\n")
for i, l := range lines {
lines[i] = strings.TrimSuffix(l, "\r")
}
return lines
}
// buildHumanContext renders the human-readable multi-line context of an
// error: a window of up to 3 lines before and after the error line, with
// the error position underlined.
func buildHumanContext(document []byte, errLineIdx, errColumn, highlightLen int, message string) string {
lines := docLines(document)
const window = 3
firstIdx := errLineIdx - window
if firstIdx < 0 {
firstIdx = 0
}
lastIdx := errLineIdx + window
if lastIdx > len(lines)-1 {
lastIdx = len(lines) - 1
}
// Empty lines at the edges of the window are dropped, unless the error
// is about that very position.
for firstIdx < errLineIdx && lines[firstIdx] == "" {
firstIdx++
}
for lastIdx > errLineIdx && lines[lastIdx] == "" {
lastIdx--
}
// Width of the column of line numbers.
width := len(strconv.Itoa(lastIdx + 1))
var buf strings.Builder
writeLine := func(idx int) {
number := strconv.Itoa(idx + 1)
for i := len(number); i < width; i++ {
buf.WriteByte(' ')
}
buf.WriteString(number)
buf.WriteByte('|')
if len(lines[idx]) > 0 {
buf.WriteByte(' ')
buf.WriteString(lines[idx])
}
buf.WriteByte('\n')
}
for idx := firstIdx; idx <= errLineIdx; idx++ {
writeLine(idx)
}
// Underline the error.
for i := 0; i < width; i++ {
buf.WriteByte(' ')
}
buf.WriteString("| ")
for i := 1; i < errColumn; i++ {
buf.WriteByte(' ')
}
// The highlight cannot extend past the end of its line.
tildes := highlightLen
if errLineIdx < len(lines) {
if avail := len(lines[errLineIdx]) - errColumn + 1; tildes > avail {
tildes = avail
}
}
if tildes < 1 {
tildes = 1
}
for i := 0; i < tildes; i++ {
buf.WriteByte('~')
}
if message != "" {
buf.WriteByte(' ')
buf.WriteString(message)
}
buf.WriteByte('\n')
for idx := errLineIdx + 1; idx <= lastIdx; idx++ {
writeLine(idx)
}
return strings.TrimSuffix(buf.String(), "\n")
}

View File

@@ -1,42 +0,0 @@
package characters
var invalidASCIITable = [256]bool{
0x00: true,
0x01: true,
0x02: true,
0x03: true,
0x04: true,
0x05: true,
0x06: true,
0x07: true,
0x08: true,
// 0x09 TAB
// 0x0A LF
0x0B: true,
0x0C: true,
// 0x0D CR
0x0E: true,
0x0F: true,
0x10: true,
0x11: true,
0x12: true,
0x13: true,
0x14: true,
0x15: true,
0x16: true,
0x17: true,
0x18: true,
0x19: true,
0x1A: true,
0x1B: true,
0x1C: true,
0x1D: true,
0x1E: true,
0x1F: true,
// 0x20 - 0x7E Printable ASCII characters
0x7F: true,
}
func InvalidASCII(b byte) bool {
return invalidASCIITable[b]
}

View File

@@ -1,175 +0,0 @@
// Package characters provides functions for working with string encodings.
package characters
import (
"unicode/utf8"
)
// Utf8TomlValidAlreadyEscaped verifies 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 slice is empty if the string is valid, or contains the bytes
// 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) []byte {
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
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) {
return p[i : i+1]
}
}
p = p[8:]
}
n := len(p)
for i := 0; i < n; {
pi := p[i]
if pi < utf8.RuneSelf {
if InvalidASCII(pi) {
return p[i : i+1]
}
i++
continue
}
x := first[pi]
if x == xx {
// Illegal starter byte.
return p[i : i+1]
}
size := int(x & 7)
if i+size > n {
// Short or invalid.
return p[i:n]
}
accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c {
return p[i : i+2]
} else if size == 2 { //revive:disable:empty-block
} else if c := p[i+2]; c < locb || hicb < c {
return p[i : i+3]
} else if size == 3 { //revive:disable:empty-block
} else if c := p[i+3]; c < locb || hicb < c {
return p[i : i+4]
}
i += size
}
return nil
}
// Utf8ValidNext returns 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 { //nolint:revive
} else if c := p[2]; c < locb || hicb < c {
return 0
} else if size == 3 { //nolint:revive
} else if c := p[3]; c < locb || hicb < c {
return 0
}
return size
}
// 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
)

View File

@@ -0,0 +1,37 @@
// Package parserbridge exposes the unstable parser's non-AST scanners to the
// root toml package without making them part of the unstable public API.
//
// The fused generic-decode fast path needs to scan keys, scalars and comments
// (and parse container values into the arena) without going through the
// AST-pushing NextExpression/Expression methods. Those scanners depend on
// Parser internals (the string-unescape scratch buffer and the node arena), so
// they have to live in the unstable package; but they are an implementation
// detail of the decoder, not something we want to commit to in the public API.
//
// The unstable package populates these variables in its init; the toml package
// reads them. The parser is passed as an any (it is always an *unstable.Parser)
// and the scalar kind is an int (it is always an unstable.Kind) so that this
// package imports neither unstable nor toml, avoiding an import cycle. Passing
// a pointer through an interface does not allocate, so the fused path keeps its
// allocation profile.
package parserbridge
var (
// ScanScalar scans a single scalar value (string, integer, float, bool or
// date/time) without building an AST node. kind is an unstable.Kind.
ScanScalar func(p any, b []byte) (kind int, raw, value, rest []byte, err error)
// ScanKey scans a (possibly dotted) key without building AST nodes,
// appending each decoded part to dst.
ScanKey func(p any, b []byte, dst [][]byte) (parts [][]byte, raw, rest []byte, err error)
// ScanComment scans a comment starting at '#', returning the comment bytes
// (including '#', excluding the line ending) and the rest of the input. It
// needs no parser state.
ScanComment func(b []byte) (comment, rest []byte, err error)
// ParseValue parses a single value (including arrays and inline tables) into
// the parser arena, returning the root *unstable.Node and the rest of the
// input.
ParseValue func(p any, b []byte) (node any, rest []byte, err error)
)

View File

@@ -14,7 +14,8 @@ func (t *KeyTracker) UpdateTable(node *unstable.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 *unstable.Node) {
t.reset()
t.Push(node)

View File

@@ -3,7 +3,6 @@ package tracker
import (
"bytes"
"fmt"
"sync"
"github.com/pelletier/go-toml/v2/unstable"
)
@@ -12,9 +11,21 @@ type keyKind uint8
const (
invalidKind keyKind = iota
// valueKind is a regular value (scalar, array, or inline table). It
// cannot be extended.
valueKind
// kvTableKind is a table created implicitly by a dotted key. It can only
// be extended by other dotted keys.
kvTableKind
// tableKind is a table created by a [header]. The explicit flag tells
// whether the table was created by its own header (true) or as an
// intermediate step of a longer key (false).
tableKind
// arrayTableKind is an array of tables created by [[header]].
arrayTableKind
// anonymousKind is an entry that cannot be looked up by name. It serves
// as the parent of the content of inline tables stored inside arrays.
anonymousKind
)
func (k keyKind) String() string {
@@ -23,22 +34,36 @@ func (k keyKind) String() string {
return "invalid"
case valueKind:
return "value"
case kvTableKind:
return "kv-table"
case tableKind:
return "table"
case arrayTableKind:
return "array table"
return "array-table"
case anonymousKind:
return "anonymous"
}
panic("missing keyKind string mapping")
}
// entry represents a node that has been seen in the document. Its size has a
// direct impact on the performance of unmarshaling documents: keep it as
// small as possible.
type entry struct {
parent int32
kind keyKind
explicit bool
name []byte
}
// SeenTracker tracks which keys have been seen with which TOML type to flag
// 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.
// 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
@@ -53,307 +78,373 @@ func (k keyKind) String() string {
// invariant above, the deletion process needs to keep the order of entries.
// This results in more copies in that case.
type SeenTracker struct {
entries []entry
currentIdx int
entries []entry
currentTable int32
// scratch buffers for clear()
removedBuf []bool
remapBuf []int32
}
var pool = sync.Pool{
New: func() interface{} {
return &SeenTracker{}
},
// Reset brings the tracker to its initial state, with just a root table, so
// that it can be reused across documents.
func (s *SeenTracker) Reset() {
s.reset()
}
// reset brings the tracker to its initial state, with just a root table.
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
s.entries = append(s.entries[:0], entry{
parent: -1,
kind: tableKind,
})
s.currentTable = 0
}
type entry struct {
// Use -1 to indicate no child or no sibling.
child int
next int
name []byte
kind keyKind
explicit bool
kv bool
}
// Find the index of the child of parentIdx with key k. Returns -1 if
// it does not exist.
func (s *SeenTracker) find(parentIdx int, k []byte) int {
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
if bytes.Equal(s.entries[i].name, k) {
return i
// find returns the id of the entry with the given parent and name, or -1.
// Anonymous entries are never returned.
func (s *SeenTracker) find(parent int32, name []byte) int32 {
// Children always appear after their parent.
for i := int(parent) + 1; i < len(s.entries); i++ {
e := &s.entries[i]
if e.parent == parent && e.kind != anonymousKind && bytes.Equal(e.name, name) {
return int32(i) //nolint:gosec // entry counts are bounded by document size
}
}
return -1
}
// Remove all descendants of node at position idx.
func (s *SeenTracker) clear(idx int) {
if idx >= len(s.entries) {
return
}
for i := s.entries[idx].child; i >= 0; {
next := s.entries[i].next
n := s.entries[0].next
s.entries[0].next = i
s.entries[i].next = n
s.entries[i].name = nil
s.clear(i)
i = next
}
s.entries[idx].child = -1
}
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool, kv bool) int {
e := entry{
child: -1,
next: s.entries[parentIdx].child,
name: name,
// create appends a new entry and returns its id.
func (s *SeenTracker) create(parent int32, name []byte, kind keyKind, explicit bool) int32 {
id := int32(len(s.entries)) //nolint:gosec // entry counts are bounded by document size
s.entries = append(s.entries, entry{
parent: parent,
kind: kind,
explicit: explicit,
kv: kv,
}
var idx int
if s.entries[0].next >= 0 {
idx = s.entries[0].next
s.entries[0].next = s.entries[idx].next
s.entries[idx] = e
} else {
idx = len(s.entries)
s.entries = append(s.entries, e)
}
s.entries[parentIdx].child = idx
return idx
name: name,
})
return id
}
func (s *SeenTracker) setExplicitFlag(parentIdx int) {
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
if s.entries[i].kv {
s.entries[i].explicit = true
s.entries[i].kv = false
}
s.setExplicitFlag(i)
// clear removes all the descendants of the entry with the given id, keeping
// the order of the remaining entries.
func (s *SeenTracker) clear(id int32) {
// Compute which entries are removed. Given that children always appear
// after their parent, a single forward pass is enough.
if cap(s.removedBuf) < len(s.entries) {
s.removedBuf = make([]bool, len(s.entries))
s.remapBuf = make([]int32, len(s.entries))
}
removed := s.removedBuf[:len(s.entries)]
remap := s.remapBuf[:len(s.entries)]
for i := range removed {
removed[i] = false
}
n := int32(0)
for i := 0; i < len(s.entries); i++ {
parent := s.entries[i].parent
if parent >= 0 && (parent == id && s.entries[i].kind != invalidKind || removed[parent]) {
removed[i] = true
continue
}
remap[i] = n
if int32(i) != n { //nolint:gosec // entry counts are bounded by document size
e := s.entries[i]
e.parent = remap[e.parent]
s.entries[n] = e
}
n++
}
s.entries = s.entries[:n]
}
// CheckExpression takes a top-level node and checks that it does not contain
// keys that have been seen in previous calls, and validates that types are
// consistent. It returns true if it is the first time this node's key is seen.
// Useful to clear array tables on first use.
// consistent. It returns true if it is the first time this node's key is
// seen. Useful to clear array tables on first use.
func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) {
if s.entries == nil {
if len(s.entries) == 0 {
s.reset()
}
switch node.Kind {
case unstable.KeyValue:
return s.checkKeyValue(node)
return false, s.checkKeyValue(s.currentTable, node)
case unstable.Table:
return s.checkTable(node)
case unstable.ArrayTable:
return s.checkArrayTable(node)
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
return false, fmt.Errorf("toml: unexpected expression kind %s", node.Kind)
}
}
// CheckTable validates a [table] header given the decoded parts of its key.
// It mirrors checkTable but is driven directly from the key parts instead of
// an AST, for callers that decode without building one. It returns whether the
// table is seen for the first time.
func (s *SeenTracker) CheckTable(parts [][]byte) (bool, error) {
parent := int32(0)
for k := 0; k < len(parts); k++ {
name := parts[k]
if k == len(parts)-1 {
// Final part of the key.
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, true)
s.currentTable = i
return true, nil
}
e := &s.entries[i]
switch e.kind {
case tableKind:
if e.explicit {
return false, fmt.Errorf("toml: table %s already exists", name)
}
e.explicit = true
s.currentTable = i
return false, nil
case kvTableKind:
return false, fmt.Errorf("toml: table %s already exists as defined by a dotted key", name)
case arrayTableKind:
return false, fmt.Errorf("toml: table %s already exists as an array of tables", name)
default:
return false, fmt.Errorf("toml: key %s should be a table, not a %s", name, e.kind)
}
}
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, false)
} else {
switch s.entries[i].kind {
case tableKind, arrayTableKind, kvTableKind:
// Tables created by dotted keys can receive new sub-tables,
// but cannot be redefined (handled by the last-part case).
default:
return false, fmt.Errorf("toml: key %s already exists as a value", name)
}
}
parent = i
}
panic("unreachable: table expression without key")
}
// CheckArrayTable validates a [[array table]] header given the decoded parts
// of its key. It mirrors checkArrayTable but is driven directly from the key
// parts. It returns whether the array table is seen for the first time.
func (s *SeenTracker) CheckArrayTable(parts [][]byte) (bool, error) {
parent := int32(0)
for k := 0; k < len(parts); k++ {
name := parts[k]
if k == len(parts)-1 {
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, arrayTableKind, true)
s.currentTable = i
return true, nil
}
if s.entries[i].kind != arrayTableKind {
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", name, s.entries[i].kind)
}
// Make the descendants of this array table re-discoverable for
// the new element.
s.clear(i)
s.currentTable = i
return false, nil
}
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, false)
} else {
switch s.entries[i].kind {
case tableKind, arrayTableKind, kvTableKind:
// Tables created by dotted keys can receive new sub-tables,
// but cannot be redefined (handled by the last-part case).
default:
return false, fmt.Errorf("toml: key %s already exists as a value", name)
}
}
parent = i
}
panic("unreachable: array table expression without key")
}
// CheckKeyValue validates the (possibly dotted) key of a key-value under the
// current table, WITHOUT validating its value. It returns the id of the leaf
// entry, so the caller can validate a container value with CheckValueUnder.
func (s *SeenTracker) CheckKeyValue(parts [][]byte) (int32, error) {
parent := s.currentTable
for k := 0; k < len(parts); k++ {
name := parts[k]
if k == len(parts)-1 {
if i := s.find(parent, name); i >= 0 {
return -1, fmt.Errorf("toml: key %s is already defined", name)
}
return s.create(parent, name, valueKind, false), nil
}
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, kvTableKind, false)
} else if s.entries[i].kind != kvTableKind {
return -1, fmt.Errorf("toml: key %s is already defined", name)
}
parent = i
}
panic("unreachable: key-value expression without key")
}
// CheckValueUnder validates the content of a value stored under the given
// entry (typically the leaf returned by CheckKeyValue): inline tables cannot
// contain duplicate keys, including in the inline tables and arrays they
// contain.
func (s *SeenTracker) CheckValueUnder(parent int32, value *unstable.Node) error {
return s.checkValue(parent, value)
}
func (s *SeenTracker) checkTable(node *unstable.Node) (bool, error) {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
parent := int32(0)
it := node.Key()
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.
// Handle the intermediate parts of the key.
for it.Next() {
part := it.Node()
name := part.Data
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 false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
// Final part of the key.
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, true)
s.currentTable = i
return true, nil
}
e := &s.entries[i]
switch e.kind {
case tableKind:
if e.explicit {
return false, fmt.Errorf("toml: table %s already exists", name)
}
e.explicit = true
s.currentTable = i
return false, nil
case kvTableKind:
return false, fmt.Errorf("toml: table %s already exists as defined by a dotted key", name)
case arrayTableKind:
return false, fmt.Errorf("toml: table %s already exists as an array of tables", name)
default:
return false, fmt.Errorf("toml: key %s should be a table, not a %s", name, e.kind)
}
}
parentIdx = idx
}
k := it.Node().Data
idx := s.find(parentIdx, k)
first := false
if idx >= 0 {
kind := s.entries[idx].kind
if kind != tableKind {
return false, fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, false)
} else {
switch s.entries[i].kind {
case tableKind, arrayTableKind, kvTableKind:
// Tables created by dotted keys can receive new sub-tables,
// but cannot be redefined (handled by the last-part case).
default:
return false, fmt.Errorf("toml: key %s already exists as a value", name)
}
}
if s.entries[idx].explicit {
return false, fmt.Errorf("toml: table %s already exists", string(k))
}
s.entries[idx].explicit = true
} else {
idx = s.create(parentIdx, k, tableKind, true, false)
first = true
parent = i
}
s.currentIdx = idx
return first, nil
panic("unreachable: table expression without key")
}
func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) {
if s.currentIdx >= 0 {
s.setExplicitFlag(s.currentIdx)
}
parent := int32(0)
it := node.Key()
parentIdx := 0
for it.Next() {
part := it.Node()
name := part.Data
if it.IsLast() {
break
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, arrayTableKind, true)
s.currentTable = i
return true, nil
}
if s.entries[i].kind != arrayTableKind {
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", name, s.entries[i].kind)
}
// Make the descendants of this array table re-discoverable for
// the new element.
s.clear(i)
// Note: clear cannot move i because i comes before all its
// descendants.
s.currentTable = i
return false, nil
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false, false)
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, tableKind, false)
} else {
entry := s.entries[idx]
if entry.kind == valueKind {
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
switch s.entries[i].kind {
case tableKind, arrayTableKind, kvTableKind:
// Tables created by dotted keys can receive new sub-tables,
// but cannot be redefined (handled by the last-part case).
default:
return false, fmt.Errorf("toml: key %s already exists as a value", name)
}
}
parentIdx = idx
parent = i
}
k := it.Node().Data
idx := s.find(parentIdx, k)
firstTime := idx < 0
if firstTime {
idx = s.create(parentIdx, k, arrayTableKind, true, false)
} else {
kind := s.entries[idx].kind
if kind != arrayTableKind {
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
}
s.clear(idx)
}
s.currentIdx = idx
return firstTime, nil
panic("unreachable: array table expression without key")
}
func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
parentIdx := s.currentIdx
func (s *SeenTracker) checkKeyValue(parent int32, node *unstable.Node) error {
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]
switch {
case it.IsLast():
return false, fmt.Errorf("toml: key %s is already defined", string(k))
case entry.kind != tableKind:
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
case entry.explicit:
return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
part := it.Node()
name := part.Data
if it.IsLast() {
if i := s.find(parent, name); i >= 0 {
return fmt.Errorf("toml: key %s is already defined", name)
}
id := s.create(parent, name, valueKind, false)
return s.checkValue(id, node.Value())
}
parentIdx = idx
i := s.find(parent, name)
if i < 0 {
i = s.create(parent, name, kvTableKind, false)
} else if s.entries[i].kind != kvTableKind {
return fmt.Errorf("toml: key %s is already defined", name)
}
parent = i
}
panic("unreachable: key-value expression without key")
}
s.entries[parentIdx].kind = valueKind
value := node.Value()
// checkValue verifies the content of a value: inline tables cannot contain
// duplicate keys, including in the inline tables and arrays they contain.
func (s *SeenTracker) checkValue(id int32, value *unstable.Node) error {
switch value.Kind {
case unstable.InlineTable:
return s.checkInlineTable(value)
it := value.Children()
for it.Next() {
if err := s.checkKeyValue(id, it.Node()); err != nil {
return err
}
}
case unstable.Array:
return s.checkArray(value)
it := value.Children()
for it.Next() {
elem := it.Node()
if elem.Kind == unstable.InlineTable || elem.Kind == unstable.Array {
elemID := s.create(id, nil, anonymousKind, false)
if err := s.checkValue(elemID, elem); err != nil {
return err
}
}
}
default:
return false, nil
}
}
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
it := node.Children()
for it.Next() {
n := it.Node()
switch n.Kind { //nolint:exhaustive
case unstable.InlineTable:
first, err = s.checkInlineTable(n)
if err != nil {
return false, err
}
case unstable.Array:
first, err = s.checkArray(n)
if err != nil {
return false, err
}
}
}
return first, nil
}
func (s *SeenTracker) checkInlineTable(node *unstable.Node) (first bool, err error) {
s = pool.Get().(*SeenTracker)
s.reset()
it := node.Children()
for it.Next() {
n := it.Node()
first, err = s.checkKeyValue(n)
if err != nil {
return false, 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 first, nil
return nil
}

View File

@@ -62,7 +62,7 @@ func (d LocalTime) String() string {
} 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")
s += strings.TrimRight(fmt.Sprintf(".%09d", d.Nanosecond), "0")
}
return s
@@ -77,7 +77,7 @@ func (d LocalTime) MarshalText() ([]byte, error) {
func (d *LocalTime) UnmarshalText(b []byte) error {
res, left, err := parseLocalTime(b)
if err == nil && len(left) != 0 {
err = unstable.NewParserError(left, "extra characters")
err = unstable.NewParserError(left, "extra characters at the end of a local time")
}
if err != nil {
return err
@@ -111,12 +111,11 @@ func (d LocalDateTime) MarshalText() ([]byte, error) {
func (d *LocalDateTime) UnmarshalText(data []byte) error {
res, left, err := parseLocalDateTime(data)
if err == nil && len(left) != 0 {
err = unstable.NewParserError(left, "extra characters")
err = unstable.NewParserError(left, "extra characters at the end of a local date time")
}
if err != nil {
return err
}
*d = res
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,69 +11,63 @@ type strict struct {
// Tracks the current key being processed.
key tracker.KeyTracker
missing []unstable.ParserError
// Reference to the document for computing key ranges.
doc []byte
missing []decodeError
}
// decodeError is the information needed to materialize a DecodeError once the
// whole document is available.
type decodeError struct {
highlight unstable.Range
key Key
message string
}
// Reset clears the state of the tracker so it can be reused for another
// document.
func (s *strict) Reset() {
s.key = tracker.KeyTracker{}
s.missing = s.missing[:0]
}
// EnterTable is called when a new table or array table expression starts
// being processed.
func (s *strict) EnterTable(node *unstable.Node) {
if !s.Enabled {
return
}
s.key.UpdateTable(node)
}
func (s *strict) EnterArrayTable(node *unstable.Node) {
if !s.Enabled {
return
}
s.key.UpdateArrayTable(node)
}
func (s *strict) EnterKeyValue(node *unstable.Node) {
if !s.Enabled {
return
}
s.key.Push(node)
}
func (s *strict) ExitKeyValue(node *unstable.Node) {
if !s.Enabled {
return
}
s.key.Pop(node)
}
// MissingTable is called when a table is present in the document but has no
// corresponding field in the target.
func (s *strict) MissingTable(node *unstable.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, unstable.ParserError{
Highlight: s.keyLocation(node),
Message: "missing table",
Key: s.key.Key(),
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
key: s.key.Key(),
message: "missing table",
})
}
// MissingField is called when a key-value is present in the document but has
// no corresponding field in the target.
func (s *strict) MissingField(node *unstable.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, unstable.ParserError{
Highlight: s.keyLocation(node),
Message: "unknown field",
Key: s.key.Key(),
s.key.Push(node)
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
key: s.key.Key(),
message: "unknown field",
})
s.key.Pop(node)
}
func (s *strict) Error(doc []byte) error {
// Error returns the cumulated StrictMissingError for the document, or nil.
func (s *strict) Error(document []byte) error {
if !s.Enabled || len(s.missing) == 0 {
return nil
}
@@ -83,14 +77,16 @@ func (s *strict) Error(doc []byte) error {
}
for _, derr := range s.missing {
derr := derr
err.Errors = append(err.Errors, *wrapDecodeError(doc, &derr))
highlight := document[derr.highlight.Offset : derr.highlight.Offset+derr.highlight.Length]
err.Errors = append(err.Errors, *newDecodeError(document, highlight, derr.key, derr.message))
}
return err
}
func (s *strict) keyLocation(node *unstable.Node) []byte {
// keyLocation returns the range of the document covering all the parts of
// the key of the given node.
func keyLocation(node *unstable.Node) unstable.Range {
k := node.Key()
hasOne := k.Next()
@@ -98,17 +94,15 @@ func (s *strict) keyLocation(node *unstable.Node) []byte {
panic("should not be called with empty key")
}
// Get the range from the first key to the last key.
firstRaw := k.Node().Raw
lastRaw := firstRaw
start := k.Node().Raw
end := start
for k.Next() {
lastRaw = k.Node().Raw
end = k.Node().Raw
}
// Compute the slice from the document using the ranges.
start := firstRaw.Offset
end := lastRaw.Offset + lastRaw.Length
return s.doc[start:end]
return unstable.Range{
Offset: start.Offset,
Length: end.Offset + end.Length - start.Offset,
}
}

View File

@@ -36,7 +36,7 @@ newline =/ %x0D.0A ; CRLF
comment-start-symbol = %x23 ; #
non-ascii = %x80-D7FF / %xE000-10FFFF
non-eol = %x09 / %x20-7F / non-ascii
non-eol = %x09 / %x20-7E / non-ascii
comment = comment-start-symbol *non-eol
@@ -74,12 +74,14 @@ escape = %x5C ; \
escape-seq-char = %x22 ; " quotation mark U+0022
escape-seq-char =/ %x5C ; \ reverse solidus U+005C
escape-seq-char =/ %x62 ; b backspace U+0008
escape-seq-char =/ %x65 ; e escape U+001B
escape-seq-char =/ %x66 ; f form feed U+000C
escape-seq-char =/ %x6E ; n line feed U+000A
escape-seq-char =/ %x72 ; r carriage return U+000D
escape-seq-char =/ %x74 ; t tab U+0009
escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
escape-seq-char =/ %x78 2HEXDIG ; xHH U+00HH
escape-seq-char =/ %x75 4HEXDIG ; uHHHH U+HHHH
escape-seq-char =/ %x55 8HEXDIG ; UHHHHHHHH U+HHHHHHHH
;; Multiline Basic String
@@ -174,7 +176,7 @@ time-secfrac = "." 1*DIGIT
time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ]
partial-time = time-hour ":" time-minute [ ":" time-second [ time-secfrac ] ]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
@@ -221,13 +223,14 @@ std-table-close = ws %x5D ; ] Right square bracket
;; Inline Table
inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
inline-table = inline-table-open [ inline-table-keyvals ] ws-comment-newline inline-table-close
inline-table-open = %x7B ws ; {
inline-table-close = ws %x7D ; }
inline-table-sep = ws %x2C ws ; , Comma
inline-table-open = %x7B ; {
inline-table-close = %x7D ; }
inline-table-sep = %x2C ; , Comma
inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
inline-table-keyvals = ws-comment-newline keyval ws-comment-newline inline-table-sep inline-table-keyvals
inline-table-keyvals =/ ws-comment-newline keyval ws-comment-newline [ inline-table-sep ]
;; Array Table

View File

@@ -6,17 +6,18 @@ import (
"time"
)
// isZeroer is used to check if a type has a custom IsZero method.
// This allows custom types to define their own zero-value semantics.
// isZeroer is used to check whether a value is the zero value for its type,
// as defined by the type itself.
type isZeroer interface {
IsZero() bool
}
var isZeroerType = reflect.TypeOf(new(isZeroer)).Elem()
var (
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
timeType = reflect.TypeOf(time.Time{})
textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
stringType = reflect.TypeOf("")

File diff suppressed because it is too large Load Diff

View File

@@ -17,43 +17,30 @@ import (
// // do something with n
// }
type Iterator struct {
nodes *[]Node
idx int32
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a
// node, false otherwise.
// Next moves the iterator forward and returns true if points to a node, false
// otherwise.
func (c *Iterator) Next() bool {
if c.nodes == nil {
return false
}
nodes := *c.nodes
if !c.started {
c.started = true
} else {
idx := c.idx
if idx >= 0 && int(idx) < len(nodes) {
c.idx = nodes[idx].next
}
} else if c.node.Valid() {
c.node = c.node.Next()
}
return c.idx >= 0 && int(c.idx) < len(nodes)
return c.node.Valid()
}
// IsLast returns true if the current node of the iterator is the last
// one. Subsequent calls to Next() will return false.
// one. Subsequent calls to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.nodes == nil || c.idx < 0 || (*c.nodes)[c.idx].next < 0
return c.node.next == 0
}
// Node returns a pointer to the node pointed at by the iterator.
func (c *Iterator) Node() *Node {
if c.nodes == nil || c.idx < 0 {
return nil
}
n := &(*c.nodes)[c.idx]
n.nodes = c.nodes
return n
return c.node
}
// Node in a TOML expression AST.
@@ -64,8 +51,8 @@ func (c *Iterator) Node() *Node {
// - Array have one child per element in the array.
// - InlineTable have one child per key-value in the table (each of kind
// InlineTable).
// - KeyValue have at least two children. The first one is the value. The rest
// make a potentially dotted key.
// - KeyValue have at least two children. The first one is the value. The
// rest make a potentially dotted key.
// - Table and ArrayTable's children represent a dotted key (same as
// KeyValue, but without the first node being the value).
//
@@ -76,68 +63,56 @@ type Node struct {
Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input).
// Absolute indices into the backing nodes slice. -1 means none.
next int32
child int32
// Reference to the backing nodes slice for navigation.
nodes *[]Node
}
// Range of bytes in the document.
type Range struct {
Offset uint32
Length uint32
// References to other nodes, as 1-based indexes into the parser's arena.
// 0 means no node.
parser *Parser
next int32
child int32
}
// Next returns a pointer to the next node, or nil if there is no next node.
func (n *Node) Next() *Node {
if n.next < 0 {
if n.next == 0 {
return nil
}
next := &(*n.nodes)[n.next]
next.nodes = n.nodes
return next
return &n.parser.nodes[n.next-1]
}
// Child returns a pointer to the first child node of this node. Other children
// can be accessed calling Next on the first child. Returns nil if this Node
// has no child.
// can be accessed calling Next on the first child. Returns nil if there is no
// child node.
func (n *Node) Child() *Node {
if n.child < 0 {
if n.child == 0 {
return nil
}
child := &(*n.nodes)[n.child]
child.nodes = n.nodes
return child
return &n.parser.nodes[n.child-1]
}
// Valid returns true if the node's kind is set (not to Invalid).
func (n *Node) Valid() bool {
return n != nil
return n != nil && n.Kind != Invalid
}
// Key returns the children nodes making the Key on a supported node. Panics
// otherwise. They are guaranteed to be all be of the Kind Key. A simple key
// otherwise. They are guaranteed to be all be of the Kind Key. A simple key
// would return just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
child := n.child
if child < 0 {
value := n.Child()
if !value.Valid() {
panic(errors.New("KeyValue should have at least two children"))
}
valueNode := &(*n.nodes)[child]
return Iterator{nodes: n.nodes, idx: valueNode.next}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{nodes: n.nodes, idx: n.child}
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil. Panics if not called on a KeyValue node,
// Guaranteed to be non-nil. Panics if not called on a KeyValue node,
// or if the Children are malformed.
func (n *Node) Value() *Node {
return n.Child()
@@ -145,5 +120,5 @@ func (n *Node) Value() *Node {
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{nodes: n.nodes, idx: n.child}
return Iterator{node: n.Child()}
}

View File

@@ -0,0 +1,21 @@
package unstable
import "github.com/pelletier/go-toml/v2/internal/parserbridge"
// Expose the non-AST scanners to the root toml package without committing to
// them in the public API. See internal/parserbridge for the rationale.
//
//nolint:gochecknoinits // load-time wiring of an internal bridge (see internal/parserbridge)
func init() {
parserbridge.ScanScalar = func(p any, b []byte) (kind int, raw, value, rest []byte, err error) {
k, raw, value, rest, err := p.(*Parser).scanScalar(b)
return int(k), raw, value, rest, err
}
parserbridge.ScanKey = func(p any, b []byte, dst [][]byte) (parts [][]byte, raw, rest []byte, err error) {
return p.(*Parser).scanKey(b, dst)
}
parserbridge.ScanComment = scanComment
parserbridge.ParseValue = func(p any, b []byte) (node any, rest []byte, err error) {
return p.(*Parser).parseValue(b)
}
}

View File

@@ -1,64 +0,0 @@
package unstable
// root contains a full AST.
//
// It is immutable once constructed with Builder.
type root struct {
nodes []Node
}
func (r *root) at(idx reference) *Node {
return &r.nodes[idx]
}
type reference int
const invalidReference reference = -1
func (r reference) Valid() bool {
return r != invalidReference
}
type builder struct {
tree root
lastIdx int
}
func (b *builder) NodeAt(ref reference) *Node {
n := b.tree.at(ref)
n.nodes = &b.tree.nodes
return n
}
func (b *builder) Reset() {
b.tree.nodes = b.tree.nodes[:0]
b.lastIdx = 0
}
func (b *builder) Push(n Node) reference {
b.lastIdx = len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n)
return reference(b.lastIdx)
}
func (b *builder) PushAndChain(n Node) reference {
newIdx := len(b.tree.nodes)
n.next = -1
n.child = -1
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = int32(newIdx) //nolint:gosec // TOML ASTs are small
}
b.lastIdx = newIdx
return reference(b.lastIdx)
}
func (b *builder) AttachChild(parent reference, child reference) {
b.tree.nodes[parent].child = int32(child) //nolint:gosec // TOML ASTs are small
}
func (b *builder) Chain(from reference, to reference) {
b.tree.nodes[from].next = int32(to) //nolint:gosec // TOML ASTs are small
}

View File

@@ -33,7 +33,7 @@ const (
Float
// Integer represents an integer value.
Integer
// LocalDate represents a a local date value.
// LocalDate represents a local date value.
LocalDate
// LocalTime represents a local time value.
LocalTime
@@ -79,5 +79,5 @@ func (k Kind) String() string {
case DateTime:
return "DateTime"
}
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
panic(fmt.Errorf("Kind.String() not implemented for kind %d", int(k)))
}

View File

@@ -0,0 +1,29 @@
package unstable
// Marshaler is implemented by types that can marshal themselves into a raw
// TOML description. The returned bytes are spliced verbatim into the encoded
// document, so they must be valid TOML for the position they end up in:
//
// - A single value (string, integer, array, inline table, …) is emitted
// inline, as in `key = <raw>`.
// - One or more key-value lines (optionally with relative sub-table headers)
// is emitted as the body of a `[key]` table.
// - At the document root, the bytes are emitted as the whole document.
//
// The encoder decides between those forms by parsing the returned bytes, and
// reports an error when they are not valid TOML for their position; see
// Encoder.EnableMarshalerInterface in the root toml package. MarshalTOML can
// be called more than once for the same value during a single encode, so it
// must be deterministic.
//
// Marshaler is the encoding counterpart of Unmarshaler.
type Marshaler interface {
MarshalTOML() ([]byte, error)
}
// MarshalTOML implements Marshaler. It returns the raw TOML bytes verbatim,
// mirroring json.RawMessage.MarshalJSON. The value receiver means both
// RawMessage and *RawMessage satisfy Marshaler.
func (m RawMessage) MarshalTOML() ([]byte, error) {
return []byte(m), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,270 +0,0 @@
package unstable
import "github.com/pelletier/go-toml/v2/internal/characters"
func scanFollows(b []byte, pattern string) bool {
n := len(pattern)
return len(b) >= n && string(b[:n]) == pattern
}
func scanFollowsMultilineBasicStringDelimiter(b []byte) bool {
return scanFollows(b, `"""`)
}
func scanFollowsMultilineLiteralStringDelimiter(b []byte) bool {
return scanFollows(b, `'''`)
}
func scanFollowsTrue(b []byte) bool {
return scanFollows(b, `true`)
}
func scanFollowsFalse(b []byte) bool {
return scanFollows(b, `false`)
}
func scanFollowsInf(b []byte) bool {
return scanFollows(b, `inf`)
}
func scanFollowsNan(b []byte) bool {
return scanFollows(b, `nan`)
}
func scanUnquotedKey(b []byte) ([]byte, []byte) {
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
for i := 0; i < len(b); i++ {
if !isUnquotedKeyChar(b[i]) {
return b[:i], b[i:]
}
}
return b, b[len(b):]
}
func isUnquotedKeyChar(r byte) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
}
func scanLiteralString(b []byte) ([]byte, []byte, error) {
// literal-string = apostrophe *literal-char apostrophe
// apostrophe = %x27 ; ' apostrophe
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
for i := 1; i < len(b); {
switch b[i] {
case '\'':
return b[:i+1], b[i+1:], nil
case '\n', '\r':
return nil, nil, NewParserError(b[i:i+1], "literal strings cannot have new lines")
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character")
}
i += size
}
return nil, nil, NewParserError(b[len(b):], "unterminated literal string")
}
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
// ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
// ml-literal-string-delim
// ml-literal-string-delim = 3apostrophe
// ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
//
// mll-content = mll-char / newline
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
// mll-quotes = 1*2apostrophe
for i := 3; i < len(b); {
switch b[i] {
case '\'':
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
i += 3
// At that point we found 3 apostrophe, and i is the
// index of the byte after the third one. The scanner
// needs to be eager, because there can be an extra 2
// apostrophe that can be accepted at the end of the
// string.
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
i++
if i >= len(b) || b[i] != '\'' {
return b[:i], b[i:], nil
}
i++
if i < len(b) && b[i] == '\'' {
return nil, nil, NewParserError(b[i-3:i+1], "''' not allowed in multiline literal string")
}
return b[:i], b[i:], nil
}
case '\r':
if len(b) < i+2 {
return nil, nil, NewParserError(b[len(b):], `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, nil, NewParserError(b[i:i+2], `need a \n after \r`)
}
i += 2 // skip the \n
continue
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character")
}
i += size
}
return nil, nil, NewParserError(b[len(b):], `multiline literal string not terminated by '''`)
}
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
const lenCRLF = 2
if len(b) < lenCRLF {
return nil, nil, NewParserError(b, "windows new line expected")
}
if b[1] != '\n' {
return nil, nil, NewParserError(b, `windows new line should be \r\n`)
}
return b[:lenCRLF], b[lenCRLF:], nil
}
func scanWhitespace(b []byte) ([]byte, []byte) {
for i := 0; i < len(b); i++ {
switch b[i] {
case ' ', '\t':
continue
default:
return b[:i], b[i:]
}
}
return b, b[len(b):]
}
func scanComment(b []byte) ([]byte, []byte, error) {
// comment-start-symbol = %x23 ; #
// non-ascii = %x80-D7FF / %xE000-10FFFF
// non-eol = %x09 / %x20-7F / non-ascii
//
// comment = comment-start-symbol *non-eol
for i := 1; i < len(b); {
if b[i] == '\n' {
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, NewParserError(b[i:i+1], "invalid character in comment")
}
size := characters.Utf8ValidNext(b[i:])
if size == 0 {
return nil, nil, NewParserError(b[i:i+1], "invalid character in comment")
}
i += size
}
return b, b[len(b):], nil
}
func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// escaped = escape escape-seq-char
escaped := false
i := 1
for ; i < len(b); i++ {
switch b[i] {
case '"':
return b[:i+1], escaped, b[i+1:], nil
case '\n', '\r':
return nil, escaped, nil, NewParserError(b[i:i+1], "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[i:i+1], "need a character after \\")
}
escaped = true
i++ // skip the next character
}
}
return nil, escaped, nil, NewParserError(b[len(b):], `basic string not terminated by "`)
}
func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
// ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
//
// mlb-content = mlb-char / newline / mlb-escaped-nl
// mlb-char = mlb-unescaped / escaped
// mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline )
escaped := false
i := 3
for ; i < len(b); i++ {
switch b[i] {
case '"':
if scanFollowsMultilineBasicStringDelimiter(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], 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, nil, NewParserError(b[i-3:i+1], `""" not allowed in multiline basic string`)
}
return b[:i], escaped, b[i:], nil
}
case '\\':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[len(b):], "need a character after \\")
}
escaped = true
i++ // skip the next character
case '\r':
if len(b) < i+2 {
return nil, escaped, nil, NewParserError(b[len(b):], `need a \n after \r`)
}
if b[i+1] != '\n' {
return nil, escaped, nil, NewParserError(b[i:i+2], `need a \n after \r`)
}
i++ // skip the \n
}
}
return nil, escaped, nil, NewParserError(b[len(b):], `multiline basic string not terminated by """`)
}

View File

@@ -1,20 +1,26 @@
package unstable
// Unmarshaler is implemented by types that can unmarshal a TOML
// description of themselves. The input is a valid TOML document
// containing the relevant portion of the parsed document.
// Unmarshaler is implemented by types that can unmarshal a TOML description
// of themselves. The input is a valid TOML document containing the relevant
// portion of the parsed document.
//
// For tables (including split tables defined in multiple places),
// the data contains the raw key-value bytes from the original document
// with adjusted table headers to be relative to the unmarshaling target.
// For tables (including split tables defined in multiple places), the data
// contains the raw key-value bytes from the original document with adjusted
// table headers to be relative to the unmarshaling target.
//
// When the decoding target itself implements this interface, it receives the
// whole document — every top-level key-value as well as every table and array
// table — assembled into a single valid TOML document and delivered once.
type Unmarshaler interface {
UnmarshalTOML(data []byte) error
}
// RawMessage is a raw encoded TOML value. It implements Unmarshaler
// and can be used to delay TOML decoding or capture raw content.
// RawMessage is a raw encoded TOML value. It implements both Unmarshaler and
// Marshaler and can be used to delay TOML decoding or capture raw content,
// similar to json.RawMessage.
//
// Example usage:
// Decoding (requires Decoder.EnableUnmarshalerInterface) captures the raw TOML
// bytes for the target without decoding them:
//
// type Config struct {
// Plugin RawMessage `toml:"plugin"`
@@ -23,10 +29,15 @@ type Unmarshaler interface {
// var cfg Config
// toml.NewDecoder(r).EnableUnmarshalerInterface().Decode(&cfg)
// // cfg.Plugin now contains the raw TOML bytes for [plugin]
//
// Encoding (requires Encoder.EnableMarshalerInterface) splices the stored bytes
// back into the document verbatim:
//
// toml.NewEncoder(w).EnableMarshalerInterface().Encode(cfg)
type RawMessage []byte
// UnmarshalTOML implements Unmarshaler.
func (m *RawMessage) UnmarshalTOML(data []byte) error {
*m = append((*m)[0:0], data...)
*m = append((*m)[:0], data...)
return nil
}

4
vendor/modules.txt vendored
View File

@@ -1478,10 +1478,10 @@ github.com/package-url/packageurl-go
# github.com/pelletier/go-toml v1.9.5
## explicit; go 1.12
github.com/pelletier/go-toml
# github.com/pelletier/go-toml/v2 v2.3.1
# github.com/pelletier/go-toml/v2 v2.4.3
## explicit; go 1.21.0
github.com/pelletier/go-toml/v2
github.com/pelletier/go-toml/v2/internal/characters
github.com/pelletier/go-toml/v2/internal/parserbridge
github.com/pelletier/go-toml/v2/internal/tracker
github.com/pelletier/go-toml/v2/unstable
# github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7