p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests

Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification
is ignored. No 502. keys add writes the secret only to --out. Release binary
has no -test-allow-prefix.
This commit is contained in:
2026-09-22 21:54:01 -07:00
parent 6cb206fb27
commit 9444a35322
311 changed files with 238557 additions and 92 deletions
+2
View File
@@ -0,0 +1,2 @@
cmd/tomll/tomll
cmd/tomljson/tomljson
+4
View File
@@ -0,0 +1,4 @@
* text=auto
benchmark/benchmark.toml text eol=lf
testdata/** text eol=lf
+8
View File
@@ -0,0 +1,8 @@
test_program/test_program_bin
fuzz/
cmd/tomll/tomll
cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
dist
tests/
test-results
+76
View File
@@ -0,0 +1,76 @@
version = "2"
[linters]
default = "none"
enable = [
"asciicheck",
"bodyclose",
"dogsled",
"dupl",
"durationcheck",
"errcheck",
"errorlint",
"exhaustive",
"forbidigo",
"gochecknoinits",
"goconst",
"gocritic",
"godoclint",
"goheader",
"gomodguard",
"goprintffuncname",
"gosec",
"govet",
"importas",
"ineffassign",
"lll",
"makezero",
"mirror",
"misspell",
"nakedret",
"nilerr",
"noctx",
"nolintlint",
"perfsprint",
"prealloc",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"thelper",
"tparallel",
"unconvert",
"unparam",
"unused",
"usetesting",
"wastedassign",
"whitespace",
]
[linters.settings.exhaustive]
default-signifies-exhaustive = true
[linters.settings.lll]
line-length = 150
[[linters.exclusions.rules]]
path = ".test.go"
linters = ["goconst", "gosec"]
[[linters.exclusions.rules]]
path = "main.go"
linters = ["forbidigo"]
[[linters.exclusions.rules]]
path = "internal"
linters = ["revive"]
text = "(exported|indent-error-flow): "
[formatters]
enable = [
"gci",
"gofmt",
"gofumpt",
"goimports",
]
+128
View File
@@ -0,0 +1,128 @@
version: 2
before:
hooks:
- go mod tidy
- go fmt ./...
- go test ./...
builds:
- id: tomll
main: ./cmd/tomll
binary: tomll
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- linux_arm64
- linux_arm
- linux_riscv64
- windows_amd64
- windows_arm64
- darwin_amd64
- darwin_arm64
- id: tomljson
main: ./cmd/tomljson
binary: tomljson
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- linux_arm64
- linux_arm
- linux_riscv64
- windows_amd64
- windows_arm64
- darwin_amd64
- darwin_arm64
- id: jsontoml
main: ./cmd/jsontoml
binary: jsontoml
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
mod_timestamp: '{{ .CommitTimestamp }}'
targets:
- linux_amd64
- linux_arm64
- linux_riscv64
- linux_arm
- windows_amd64
- windows_arm64
- darwin_amd64
- darwin_arm64
universal_binaries:
- id: tomll
replace: true
name_template: tomll
- id: tomljson
replace: true
name_template: tomljson
- id: jsontoml
replace: true
name_template: jsontoml
archives:
- id: jsontoml
formats:
- tar.xz
ids:
- jsontoml
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomljson
formats:
- tar.xz
ids:
- tomljson
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
- id: tomll
formats:
- tar.xz
ids:
- tomll
files:
- none*
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
dockers_v2:
- id: tools
ids:
- jsontoml
- tomljson
- tomll
images:
- "ghcr.io/pelletier/go-toml"
tags:
- "latest"
- "{{ .Tag }}"
- "v{{ .Major }}"
platforms:
- linux/amd64
checksum:
name_template: 'sha256sums.txt'
snapshot:
version_template: "{{ incpatch .Version }}-next"
release:
github:
owner: pelletier
name: go-toml
draft: true
prerelease: auto
mode: replace
changelog:
use: github-native
announce:
skip: true
+73
View File
@@ -0,0 +1,73 @@
# Agent Guidelines for go-toml
This file provides guidelines for AI agents contributing to go-toml. All agents must follow these rules derived from [CONTRIBUTING.md](./CONTRIBUTING.md).
## Project Overview
go-toml is a TOML library for Go. The goal is to provide an easy-to-use and efficient TOML implementation that gets the job done without getting in the way.
## Code Change Rules
### Backward Compatibility
- **No backward-incompatible changes** unless explicitly discussed and approved
- Avoid breaking people's programs unless absolutely necessary
### Testing Requirements
- **All bug fixes must include regression tests**
- **All new code must be tested**
- Run tests before submitting: `go test -race ./...`
- Test coverage must not decrease. Check with:
```bash
go test -covermode=atomic -coverprofile=coverage.out
go tool cover -func=coverage.out
```
- All lines of code touched by changes should be covered by tests
### Performance Requirements
- go-toml aims to stay efficient; avoid performance regressions
- Run benchmarks to verify: `go test ./... -bench=. -count=10`
- Compare results using [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat)
### Documentation
- New features or feature extensions must include documentation
- Documentation lives in [README.md](./README.md) and throughout source code
### Code Style
- Follow existing code format and structure
- Code must pass `go fmt`
- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`):
```bash
# Install specific version (check lint.yml for current version)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin <version>
# Run linter
golangci-lint run ./...
```
### Commit Messages
- 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:
1. Tests pass (`go test -race ./...`)
2. No backward-incompatible changes (unless discussed)
3. Relevant documentation added/updated
4. No performance regression (verify with benchmarks)
5. Capabilities are not increasing (`./caps.sh check`)
6. Title is clear and understandable for changelog
+254
View File
@@ -0,0 +1,254 @@
# Contributing
Thank you for your interest in go-toml! We appreciate you considering
contributing to go-toml!
The main goal is the project is to provide an easy-to-use and efficient TOML
implementation for Go that gets the job done and gets out of your way – dealing
with TOML is probably not the central piece of your project.
As the single maintainer of go-toml, time is scarce. All help, big or small, is
more than welcomed!
## Ask questions
Any question you may have, somebody else might have it too. Always feel free to
ask them on the [discussion board][discussions]. We will try to answer them as
clearly and quickly as possible, time permitting.
Asking questions also helps us identify areas where the documentation needs
improvement, or new features that weren't envisioned before. Sometimes, a
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
away!
[discussions]: https://github.com/pelletier/go-toml/discussions
## Improve the documentation
The best way to share your knowledge and experience with go-toml is to improve
the documentation. Fix a typo, clarify an interface, add an example, anything
goes!
The documentation is present in the [README][readme] and thorough the source
code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change
to the documentation, create a pull request with your proposed changes. For
simple changes like that, the easiest way to go is probably the "Fork this
project and edit the file" button on GitHub, displayed at the top right of the
file. Unless it's a trivial change (for example a typo), provide a little bit of
context in your pull request description or commit message.
## Report a bug
Found a bug! Sorry to hear that :(. Help us and other track them down and fix by
reporting it. [File a new bug report][bug-report] on the [issues
tracker][issues-tracker]. The template should provide enough guidance on what to
include. When in doubt: add more details! By reducing ambiguity and providing
more information, it decreases back and forth and saves everyone time.
## Code changes
Want to contribute a patch? Very happy to hear that!
First, some high-level rules:
- A short proposal with some POC code is better than a lengthy piece of text
with no code. Code speaks louder than words. That being said, bigger changes
should probably start with a [discussion][discussions].
- No backward-incompatible patch will be accepted unless discussed. Sometimes
it's hard, but we try not to break people's programs unless we absolutely have
to.
- If you are writing a new feature or extending an existing one, make sure to
write some documentation.
- Bug fixes need to be accompanied with regression tests.
- New code needs to be tested.
- Your commit messages need to explain why the change is needed, even if already
included in the PR description.
It does sound like a lot, but those best practices are here to save time overall
and continuously improve the quality of the project, which is something everyone
benefits from.
### Get started
The fairly standard code contribution process looks like that:
1. [Fork the project][fork].
2. Make your changes, commit on any branch you like.
3. [Open up a pull request][pull-request]
4. Review, potential ask for changes.
5. Merge.
Feel free to ask for help! You can create draft pull requests to gather
some early feedback!
### Run the tests
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
During the pull request process, all tests will be ran on Linux, Windows, and
MacOS on the last two versions of Go.
However, given GitHub's new policy to _not_ run Actions on pull requests until a
maintainer clicks on button, it is highly recommended that you run them locally
as you make changes.
### Test across Go versions
The repository includes tooling to test go-toml across multiple Go versions
(1.11 through 1.25) both locally and in GitHub Actions.
#### Local testing with Docker
Prerequisites: Docker installed and running, Bash shell, `rsync` command.
```bash
# Test all Go versions in parallel (default)
./test-go-versions.sh
# Test specific versions
./test-go-versions.sh 1.21 1.22 1.23
# Test sequentially (slower but uses less resources)
./test-go-versions.sh --sequential
# Verbose output with custom results directory
./test-go-versions.sh --verbose --output ./my-results 1.24 1.25
# Show all options
./test-go-versions.sh --help
```
The script creates Docker containers for each Go version and runs the full test
suite. Results are saved to a `test-results/` directory with individual logs and
a comprehensive summary report.
The script only exits with a non-zero status code if either of the two most
recent Go versions fail.
#### GitHub Actions testing (maintainers)
1. Go to the **Actions** tab in the GitHub repository
2. Select **"Go Versions Compatibility Test"** from the workflow list
3. Click **"Run workflow"**
4. Optionally customize:
- **Go versions**: Space-separated list (e.g., `1.21 1.22 1.23`)
- **Execution mode**: Parallel (faster) or sequential (more stable)
### Check coverage
We use `go tool cover` to compute test coverage. Most code editors have a way to
run and display code coverage, but at the end of the day, we do this:
```
go test -covermode=atomic -coverprofile=coverage.out
go tool cover -func=coverage.out
```
and verify that the overall percentage of tested code does not go down. This is
a requirement. As a rule of thumb, all lines of code touched by your changes
should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your
code lowers the coverage.
### Verify performance
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
builtin benchmark systems. Because of their noisy nature, containers provided by
GitHub Actions cannot be reliably used for benchmarking. As a result, you are
responsible for checking that your changes do not incur a performance penalty.
You can run their following to execute benchmarks:
```
go test ./... -bench=. -count=10
```
Benchmark results should be compared against each other with
[benchstat][benchstat]. Typical flow looks like this:
1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to
a file (for example `old.txt`).
2. Make some code changes.
3. Run `go test ....` again, and save the output to an other file (for example
`new.txt`).
4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any
test.
On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts
performance.
It is highly encouraged to add the benchstat results to your pull request
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
code. We enforce using `go fmt` on the whole code base.
---
## Maintainers-only
### Merge pull request
Checklist:
- Passing CI.
- Does not introduce backward-incompatible changes (unless discussed).
- Has relevant doc changes.
- Benchstat does not show performance regression.
- Pull request is [labeled appropriately][pr-labels].
- Title will be understandable in the changelog.
1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information
nice and clean.
3. Make sure the commit title is clear and contains the PR number (#123).
### New release
1. Decide on the next version number. Use semver. Review commits since last
version to assess.
2. Tag release. For example:
```
git checkout v2
git pull
git tag v2.2.0
git push --tags
```
3. CI automatically builds a draft GitHub release. Review it and edit as
necessary. Look for "Other changes". That would indicate a pull request not
labeled properly. Tweak labels and pull request titles until changelog looks
good for users.
4. Check "create discussion" box, in the "Releases" category.
5. If new version is an alpha or beta only, check pre-release box.
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
[pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml
[readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
[new-release]: https://github.com/pelletier/go-toml/releases/new
[gh]: https://github.com/cli/cli
[pr-labels]: https://github.com/pelletier/go-toml/blob/v2/.github/release.yml
+6
View File
@@ -0,0 +1,6 @@
FROM scratch
ENV PATH "$PATH:/bin"
ARG TARGETPLATFORM
COPY $TARGETPLATFORM/tomll /bin/tomll
COPY $TARGETPLATFORM/tomljson /bin/tomljson
COPY $TARGETPLATFORM/jsontoml /bin/jsontoml
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
go-toml v2
Copyright (c) 2021 - 2023 Thomas Pelletier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+327
View File
@@ -0,0 +1,327 @@
# go-toml v2
Go library for the [TOML](https://toml.io/en/) format.
This library supports [TOML v1.1.0](https://toml.io/en/v1.1.0).
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
## Documentation
Full API, examples, and implementation notes are available in the Go
documentation.
[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
## Import
```go
import "github.com/pelletier/go-toml/v2"
```
## Features
### Stdlib behavior
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
operations should not be shockingly slow. See [benchmarks](#benchmarks).
### Strict mode
`Decoder` can be set to "strict mode", which makes it error when some parts of
the TOML document was not present in the target structure. This is a great way
to check for typos. [See example in the documentation][strict].
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.DisallowUnknownFields
### Contextualized errors
When most decoding errors occur, go-toml returns [`DecodeError`][decode-err],
which contains a human readable contextualized version of the error. For
example:
```
1| [server]
2| path = 100
| ~~~ cannot decode TOML integer into struct field toml_test.Server.Path of type string
3| port = 50
```
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
### Local date and time support
TOML supports native [local date/times][ldt]. It allows to represent a given
date, time, or date-time without relation to a timezone or offset. To support
this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`,
making them convenient yet unambiguous structures for their respective TOML
representation.
[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
### Commented config
Since TOML is often used for configuration files, go-toml can emit documents
annotated with [comments and commented-out values][comments-example]. For
example, it can generate the following file:
```toml
# Host IP to connect to.
host = '127.0.0.1'
# Port of the remote server.
port = 4242
# Encryption parameters (optional)
# [TLS]
# cipher = 'AEAD-AES128-GCM-SHA256'
# version = 'TLS 1.3'
```
[comments-example]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Marshal-Commented
## Getting started
Given the following struct, let's see how to read it and write it as TOML:
```go
type MyConfig struct {
Version int
Name string
Tags []string
}
```
### Unmarshaling
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
content.
Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_.
For example:
```go
doc := `
version = 2
name = "go-toml"
tags = ["go", "toml"]
`
var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
}
fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name)
fmt.Println("tags:", cfg.Tags)
// Output:
// version: 2
// name: go-toml
// tags: [go toml]
```
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
Here is an example using tables with some simple nesting:
```go
doc := `
age = 45
fruits = ["apple", "pear"]
# these are very important!
[my-variables]
first = 1
second = 0.2
third = "abc"
# this is not so important.
[my-variables.b]
bfirst = 123
`
var Document struct {
Age int
Fruits []string
Myvariables struct {
First int
Second float64
Third string
B struct {
Bfirst int
}
} `toml:"my-variables"`
}
err := toml.Unmarshal([]byte(doc), &Document)
if err != nil {
panic(err)
}
fmt.Println("age:", Document.Age)
fmt.Println("fruits:", Document.Fruits)
fmt.Println("my-variables.first:", Document.Myvariables.First)
fmt.Println("my-variables.second:", Document.Myvariables.Second)
fmt.Println("my-variables.third:", Document.Myvariables.Third)
fmt.Println("my-variables.B.Bfirst:", Document.Myvariables.B.Bfirst)
// Output:
// age: 45
// fruits: [apple pear]
// my-variables.first: 1
// my-variables.second: 0.2
// my-variables.third: abc
// my-variables.B.Bfirst: 123
```
### Marshaling
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
as a TOML document:
```go
cfg := MyConfig{
Version: 2,
Name: "go-toml",
Tags: []string{"go", "toml"},
}
b, err := toml.Marshal(cfg)
if err != nil {
panic(err)
}
fmt.Println(string(b))
// Output:
// Version = 2
// Name = 'go-toml'
// Tags = ['go', 'toml']
```
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
## Unstable API
This API does not yet follow the backward compatibility guarantees of this
library. They provide early access to features that may have rough edges or an
API subject to change.
### Parser
Parser is the unstable API that allows iterative parsing of a TOML document at
the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
## Benchmarks
Execution time speedup compared to other Go TOML libraries:
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>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>
<p>The table above has the results of the most common use-cases. The table below
contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>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>
</details>
## Tools
Go-toml provides three handy command line tools:
* `tomljson`: Reads a TOML file and outputs its JSON representation.
```
$ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
$ tomljson --help
```
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
```
$ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
$ jsontoml --help
```
* `tomll`: Lints and reformats a TOML file.
```
$ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
$ tomll --help
```
### Docker image
Those tools are also available as a [Docker image][docker]. For example, to use
`tomljson`:
```
docker run -i ghcr.io/pelletier/go-toml:v2 tomljson < example.toml
```
Multiple versions are available on [ghcr.io][docker].
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
## Versioning
Expect for parts explicitly marked otherwise, go-toml follows [Semantic
Versioning](https://semver.org). The supported version of
[TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this
document. The last two major versions of Go are supported (see [Go Release
Policy](https://golang.org/doc/devel/release.html#policy)).
## License
The MIT License (MIT). Read [LICENSE](LICENSE).
+16
View File
@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ---------- | ------------------ |
| Latest 2.x | :white_check_mark: |
| All 1.x | :x: |
| All 0.x | :x: |
## Reporting a Vulnerability
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
as many details as possible to reproduce the vulnerability. This is a
side-project: I will try to get back to you as quickly as possible, time
permitting in my personal life. Providing a working patch helps very much!
+1
View File
@@ -0,0 +1 @@
github.com/pelletier/go-toml/v2: CAPABILITY_REFLECT, CAPABILITY_UNANALYZED, CAPABILITY_UNSAFE_POINTER
+101
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
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env bash
stderr() {
echo "$@" 1>&2
}
usage() {
b=$(basename "$0")
echo $b: ERROR: "$@" 1>&2
cat 1>&2 <<EOF
DESCRIPTION
$(basename "$0") is the script to run continuous integration commands for
go-toml on unix.
Requires Go and Git to be available in the PATH. Expects to be ran from the
root of go-toml's Git repository.
USAGE
$b COMMAND [OPTIONS...]
COMMANDS
benchmark [OPTIONS...] [BRANCH]
Run benchmarks.
ARGUMENTS
BRANCH Optional. Defines which Git branch to use when running
benchmarks.
OPTIONS
-d Compare benchmarks of HEAD with BRANCH using benchstats. In
this form the BRANCH argument is required.
-a Compare benchmarks of HEAD against go-toml v1 and
BurntSushi/toml.
-html When used with -a, emits the output as HTML, ready to be
embedded in the README.
coverage [OPTIONS...] [BRANCH]
Generates code coverage.
ARGUMENTS
BRANCH Optional. Defines which Git branch to use when reporting
coverage. Defaults to HEAD.
OPTIONS
-d Compare coverage of HEAD with the one of BRANCH. In this form,
the BRANCH argument is required. Exit code is non-zero when
coverage percentage decreased.
EOF
exit 1
}
cover() {
branch="${1}"
dir="$(mktemp -d)"
stderr "Executing coverage for ${branch} at ${dir}"
if [ "${branch}" = "HEAD" ]; then
cp -r . "${dir}/"
else
git worktree add "$dir" "$branch"
fi
pushd "$dir"
go test -covermode=atomic -coverpkg=./... -coverprofile=coverage.out.tmp ./...
grep -Ev '(fuzz|testsuite|tomltestgen|gotoml-test-decoder|gotoml-test-encoder)' coverage.out.tmp > coverage.out
go tool cover -func=coverage.out
echo "Coverage profile for ${branch}: ${dir}/coverage.out" >&2
popd
if [ "${branch}" != "HEAD" ]; then
git worktree remove --force "$dir"
fi
}
coverage() {
case "$1" in
-d)
shift
target="${1?Need to provide a target branch argument}"
output_dir="$(mktemp -d)"
target_out="${output_dir}/target.txt"
head_out="${output_dir}/head.txt"
cover "${target}" > "${target_out}"
cover "HEAD" > "${head_out}"
cat "${target_out}"
cat "${head_out}"
echo ""
target_pct="$(tail -n2 ${target_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%.*/\1/')"
head_pct="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')"
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
echo "Delta: ${delta_pct}"
if [[ $delta_pct = \-* ]]; then
echo "Regression!";
target_diff="${output_dir}/target.diff.txt"
head_diff="${output_dir}/head.diff.txt"
cat "${target_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${target_diff}"
cat "${head_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${head_diff}"
diff --side-by-side --suppress-common-lines "${target_diff}" "${head_diff}"
return 1
fi
return 0
;;
esac
cover "${1-HEAD}"
}
bench() {
branch="${1}"
out="${2}"
replace="${3}"
dir="$(mktemp -d)"
stderr "Executing benchmark for ${branch} at ${dir}"
if [ "${branch}" = "HEAD" ]; then
cp -r . "${dir}/"
else
git worktree add "$dir" "$branch"
fi
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 ${tags} '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}"
popd
if [ "${branch}" != "HEAD" ]; then
git worktree remove --force "$dir"
fi
}
fmktemp() {
if mktemp --version &> /dev/null; then
# GNU
mktemp --suffix=-$1
else
# BSD
mktemp -t $1
fi
}
benchstathtml() {
python3 - $1 <<'EOF'
import sys
lines = []
stop = False
with open(sys.argv[1]) as f:
for line in f.readlines():
line = line.strip()
if line == "":
stop = True
if not stop:
lines.append(line.split(','))
results = []
for line in reversed(lines[2:]):
if len(line) < 8 or line[0] == "":
continue
v2 = float(line[1])
results.append([
line[0].replace("-32", ""),
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[7])/v2), # bs
])
if not results:
print("No benchmark results to display.", file=sys.stderr)
sys.exit(1)
# move geomean to the end
results.append(results[0])
del results[0]
def printtable(data):
print("""
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>""")
for r in data:
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
print(""" </tbody>
</table>""")
def match(x):
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
above = [x for x in results if match(x)]
below = [x for x in results if not match(x)]
printtable(above)
print("<details><summary>See more</summary>")
print("""<p>The table above has the results of the most common use-cases. The table below
contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>""")
printtable(below)
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
print("</details>")
EOF
}
benchmark() {
case "$1" in
-d)
shift
target="${1?Need to provide a target branch argument}"
old=`fmktemp ${target}`
bench "${target}" "${old}"
new=`fmktemp HEAD`
bench HEAD "${new}"
benchstat "${old}" "${new}"
return 0
;;
-a)
shift
v2stats=`fmktemp go-toml-v2`
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
v1stats=`fmktemp go-toml-v1`
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
bsstats=`fmktemp bs-toml`
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
cp "${v2stats}" go-toml-v2.txt
cp "${v1stats}" go-toml-v1.txt
cp "${bsstats}" bs-toml.txt
if [ "$1" = "-html" ]; then
tmpcsv=`fmktemp csv`
benchstat -format csv go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
benchstathtml $tmpcsv
else
benchstat go-toml-v2.txt go-toml-v1.txt bs-toml.txt
fi
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
return $?
esac
bench "${1-HEAD}" `mktemp`
}
case "$1" in
coverage) shift; coverage $@;;
benchmark) shift; benchmark $@;;
*) usage "bad argument $1";;
esac
+538
View File
@@ -0,0 +1,538 @@
package toml
import (
"bytes"
"fmt"
"math"
"strconv"
"time"
"github.com/pelletier/go-toml/v2/unstable"
)
func parseInteger(b []byte) (int64, error) {
if len(b) > 2 && b[0] == '0' {
switch b[1] {
case 'x':
return parseIntHex(b)
case 'b':
return parseIntBin(b)
case 'o':
return parseIntOct(b)
default:
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) {
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
return date, unstable.NewParserError(b, "dates are expected to have the format YYYY-MM-DD")
}
var err error
date.Year, err = parseDecimalDigits(b[0:4])
if err != nil {
return date, err
}
date.Month, err = parseDecimalDigits(b[5:7])
if err != nil {
return date, err
}
date.Day, err = parseDecimalDigits(b[8:10])
if err != nil {
return date, err
}
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 !isDecimalDigit(c) {
return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)")
}
v = v*10 + int(c-'0')
}
return v, nil
}
// 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
}
var zone *time.Location
if len(b) == 0 {
// 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' {
b = b[1:]
zone = time.UTC
} else {
const dateTimeByteLen = 6
if len(b) != dateTimeByteLen {
return time.Time{}, unstable.NewParserError(b, "invalid date-time timezone")
}
var direction int
switch b[0] {
case '-':
direction = -1
case '+':
direction = +1
default:
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character")
}
if b[3] != ':' {
return time.Time{}, unstable.NewParserError(b[3:4], "expected a : separator")
}
hours, err := parseDecimalDigits(b[1:3])
if err != nil {
return time.Time{}, err
}
if hours > 23 {
return time.Time{}, unstable.NewParserError(b[1:3], "invalid timezone offset hours")
}
minutes, err := parseDecimalDigits(b[4:6])
if err != nil {
return time.Time{}, err
}
if minutes > 59 {
return time.Time{}, unstable.NewParserError(b[4:6], "invalid timezone offset minutes")
}
seconds := direction * (hours*3600 + minutes*60)
if seconds == 0 {
zone = time.UTC
} else {
zone = time.FixedZone("", seconds)
}
b = b[dateTimeByteLen:]
}
if len(b) > 0 {
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
}
t := time.Date(
dt.Year,
time.Month(dt.Month),
dt.Day,
dt.Hour,
dt.Minute,
dt.Second,
dt.Nanosecond,
zone)
return t, nil
}
+362
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
}
+2
View File
@@ -0,0 +1,2 @@
// Package toml is a library to read and write TOML documents.
package toml
+233
View File
@@ -0,0 +1,233 @@
package toml
import (
"errors"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/unstable"
)
// DecodeError represents an error encountered during the parsing or decoding
// of a TOML document.
//
// In addition to the error message, it contains the position in the document
// where it happened, as well as a human-readable representation that shows
// where the error occurred in the document.
type DecodeError struct {
message string
line int
column int
key Key
human string
}
// StrictMissingError occurs in a TOML document that does not have a
// corresponding field in the target value. It contains all the missing fields
// in Errors.
//
// Emitted by Decoder when DisallowUnknownFields() was called.
type StrictMissingError struct {
// One error per field that could not be found.
Errors []DecodeError
}
// Error returns the canonical string for this error.
func (s *StrictMissingError) Error() string {
return "strict mode: fields in the document are missing in the target struct"
}
// String returns a human readable description of all errors.
func (s *StrictMissingError) String() string {
var buf strings.Builder
for i, e := range s.Errors {
if i > 0 {
buf.WriteString("\n---\n")
}
buf.WriteString(e.String())
}
return buf.String()
}
// Unwrap returns wrapped decode errors
//
// Implements errors.Join() interface.
func (s *StrictMissingError) Unwrap() []error {
errs := make([]error, len(s.Errors))
for i := range s.Errors {
errs[i] = &s.Errors[i]
}
return errs
}
// Key represents a TOML key as a sequence of key parts.
type Key []string
// Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string {
return "toml: " + e.message
}
// String returns the human-readable contextualized error. This string is
// multi-line.
func (e *DecodeError) String() string {
return e.human
}
// Position returns the (line, column) pair indicating where the error
// occurred in the document. Positions are 1-indexed.
func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}
// Key that was being processed when the error occurred.
func (e *DecodeError) Key() Key {
return e.key
}
// 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 {
if de == nil {
return nil
}
return newDecodeError(document, de.Highlight, de.Key, de.Message)
}
// 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)
errLineIdx, errColumn := positionAt(document, offset)
human := buildHumanContext(document, errLineIdx, errColumn, len(highlight), message)
return &DecodeError{
message: message,
line: errLineIdx + 1,
column: errColumn,
key: key,
human: human,
}
}
// 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")
}
@@ -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)
)
+49
View File
@@ -0,0 +1,49 @@
package tracker
import "github.com/pelletier/go-toml/v2/unstable"
// KeyTracker is a tracker that keeps track of the current Key as the AST is
// walked.
type KeyTracker struct {
k []string
}
// UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node *unstable.Node) {
t.reset()
t.Push(node)
}
// UpdateArrayTable sets the state of the tracker with the AST array table
// node.
func (t *KeyTracker) UpdateArrayTable(node *unstable.Node) {
t.reset()
t.Push(node)
}
// Push the given key on the stack.
func (t *KeyTracker) Push(node *unstable.Node) {
it := node.Key()
for it.Next() {
t.k = append(t.k, string(it.Node().Data))
}
}
// Pop key from stack.
func (t *KeyTracker) Pop(node *unstable.Node) {
it := node.Key()
for it.Next() {
t.k = t.k[:len(t.k)-1]
}
}
// Key returns the current key.
func (t *KeyTracker) Key() []string {
k := make([]string, len(t.k))
copy(k, t.k)
return k
}
func (t *KeyTracker) reset() {
t.k = t.k[:0]
}
+450
View File
@@ -0,0 +1,450 @@
package tracker
import (
"bytes"
"fmt"
"github.com/pelletier/go-toml/v2/unstable"
)
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 {
switch k {
case invalidKind:
return "invalid"
case valueKind:
return "value"
case kvTableKind:
return "kv-table"
case tableKind:
return "table"
case arrayTableKind:
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.
//
// To find whether a given key (sequence of []byte) has already been visited,
// the entries are linearly searched, looking for one with the right name and
// parent id.
//
// Given that all keys appear in the document after their parent, it is
// guaranteed that all descendants of a node are stored after the node, this
// speeds up the search process.
//
// When encountering [[array tables]], the descendants of that node are removed
// to allow that branch of the tree to be "rediscovered". To maintain the
// invariant above, the deletion process needs to keep the order of entries.
// This results in more copies in that case.
type SeenTracker struct {
entries []entry
currentTable int32
// scratch buffers for clear()
removedBuf []bool
remapBuf []int32
}
// 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() {
s.entries = append(s.entries[:0], entry{
parent: -1,
kind: tableKind,
})
s.currentTable = 0
}
// 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
}
// 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,
name: name,
})
return id
}
// 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.
func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) {
if len(s.entries) == 0 {
s.reset()
}
switch node.Kind {
case unstable.KeyValue:
return false, s.checkKeyValue(s.currentTable, node)
case unstable.Table:
return s.checkTable(node)
case unstable.ArrayTable:
return s.checkArrayTable(node)
default:
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) {
parent := int32(0)
it := node.Key()
// Handle the intermediate parts of the key.
for it.Next() {
part := it.Node()
name := part.Data
if it.IsLast() {
// 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")
}
func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) {
parent := int32(0)
it := node.Key()
for it.Next() {
part := it.Node()
name := part.Data
if it.IsLast() {
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
}
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")
}
func (s *SeenTracker) checkKeyValue(parent int32, node *unstable.Node) error {
it := node.Key()
for it.Next() {
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())
}
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")
}
// 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:
it := value.Children()
for it.Next() {
if err := s.checkKeyValue(id, it.Node()); err != nil {
return err
}
}
case unstable.Array:
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 nil
}
+2
View File
@@ -0,0 +1,2 @@
// Package tracker provides functions for keeping track of AST nodes.
package tracker
+121
View File
@@ -0,0 +1,121 @@
package toml
import (
"fmt"
"strings"
"time"
"github.com/pelletier/go-toml/v2/unstable"
)
// LocalDate represents a calendar day in no specific timezone.
type LocalDate struct {
Year int
Month int
Day int
}
// AsTime converts d into a specific time instance at midnight in zone.
func (d LocalDate) AsTime(zone *time.Location) time.Time {
return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
}
// String returns RFC 3339 representation of d.
func (d LocalDate) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
// MarshalText returns RFC 3339 representation of d.
func (d LocalDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalDate) UnmarshalText(b []byte) error {
res, err := parseLocalDate(b)
if err != nil {
return err
}
*d = res
return nil
}
// LocalTime represents a time of day of no specific day in no specific
// timezone.
type LocalTime struct {
Hour int // Hour of the day: [0; 24[
Minute int // Minute of the hour: [0; 60[
Second int // Second of the minute: [0; 59]
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
Precision int // Number of digits to display for Nanosecond.
}
// String returns RFC 3339 representation of d.
// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
// of digits for nanoseconds is provided.
func (d LocalTime) String() string {
s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
if d.Precision > 0 {
s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
} else if d.Nanosecond > 0 {
// Nanoseconds are specified, but precision is not provided. Use the
// minimum.
s += strings.TrimRight(fmt.Sprintf(".%09d", d.Nanosecond), "0")
}
return s
}
// MarshalText returns RFC 3339 representation of d.
func (d LocalTime) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalTime) UnmarshalText(b []byte) error {
res, left, err := parseLocalTime(b)
if err == nil && len(left) != 0 {
err = unstable.NewParserError(left, "extra characters at the end of a local time")
}
if err != nil {
return err
}
*d = res
return nil
}
// LocalDateTime represents a time of a specific day in no specific timezone.
type LocalDateTime struct {
LocalDate
LocalTime
}
// AsTime converts d into a specific time instance in zone.
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
}
// String returns RFC 3339 representation of d.
func (d LocalDateTime) String() string {
return d.LocalDate.String() + "T" + d.LocalTime.String()
}
// MarshalText returns RFC 3339 representation of d.
func (d LocalDateTime) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText parses b using RFC 3339 to fill d.
func (d *LocalDateTime) UnmarshalText(data []byte) error {
res, left, err := parseLocalDateTime(data)
if err == nil && len(left) != 0 {
err = 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
+108
View File
@@ -0,0 +1,108 @@
package toml
import (
"github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/unstable"
)
type strict struct {
Enabled bool
// Tracks the current key being processed.
key tracker.KeyTracker
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)
}
// 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, 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.key.Push(node)
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
key: s.key.Key(),
message: "unknown field",
})
s.key.Pop(node)
}
// 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
}
err := &StrictMissingError{
Errors: make([]DecodeError, 0, len(s.missing)),
}
for _, derr := range s.missing {
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
}
// 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()
if !hasOne {
panic("should not be called with empty key")
}
start := k.Node().Raw
end := start
for k.Next() {
end = k.Node().Raw
}
return unstable.Range{
Offset: start.Offset,
Length: end.Offset + end.Length - start.Offset,
}
}
+597
View File
@@ -0,0 +1,597 @@
#!/usr/bin/env bash
set -uo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Go versions to test (1.11 through 1.26)
GO_VERSIONS=(
"1.11"
"1.12"
"1.13"
"1.14"
"1.15"
"1.16"
"1.17"
"1.18"
"1.19"
"1.20"
"1.21"
"1.22"
"1.23"
"1.24"
"1.25"
"1.26"
)
# Default values
PARALLEL=true
VERBOSE=false
OUTPUT_DIR="test-results"
DOCKER_TIMEOUT="10m"
usage() {
cat << EOF
Usage: $0 [OPTIONS] [GO_VERSIONS...]
Test go-toml across multiple Go versions using Docker containers.
The script reports the lowest continuous supported Go version (where all subsequent
versions pass) and only exits with non-zero status if either of the two most recent
Go versions fail, indicating immediate attention is needed.
Note: For Go versions < 1.21, the script automatically updates go.mod to match the
target version, but older versions may still fail due to missing standard library
features (e.g., the 'slices' package introduced in Go 1.21).
OPTIONS:
-h, --help Show this help message
-s, --sequential Run tests sequentially instead of in parallel
-v, --verbose Enable verbose output
-o, --output DIR Output directory for test results (default: test-results)
-t, --timeout TIME Docker timeout for each test (default: 10m)
--list List available Go versions and exit
ARGUMENTS:
GO_VERSIONS Specific Go versions to test (default: all supported versions)
Examples: 1.21 1.22 1.23
EXAMPLES:
$0 # Test all Go versions in parallel
$0 --sequential # Test all Go versions sequentially
$0 1.21 1.22 1.23 # Test specific versions
$0 --verbose --output ./results 1.25 1.26 # Verbose output to custom directory
EXIT CODES:
0 Recent Go versions pass (good compatibility)
1 Recent Go versions fail (needs attention) or script error
EOF
}
log() {
echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" >&2
}
log_success() {
echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" >&2
}
log_error() {
echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" >&2
}
log_warning() {
echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" >&2
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-s|--sequential)
PARALLEL=false
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-t|--timeout)
DOCKER_TIMEOUT="$2"
shift 2
;;
--list)
echo "Available Go versions:"
printf '%s\n' "${GO_VERSIONS[@]}"
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
*)
# Remaining arguments are Go versions
break
;;
esac
done
# If specific versions provided, use those instead of defaults
if [[ $# -gt 0 ]]; then
GO_VERSIONS=("$@")
fi
# Validate Go versions
for version in "${GO_VERSIONS[@]}"; do
if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-6])$ ]]; then
log_error "Invalid Go version: $version. Supported versions: 1.11-1.26"
exit 1
fi
done
# Check if Docker is available
if ! command -v docker &> /dev/null; then
log_error "Docker is required but not installed or not in PATH"
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
log_error "Docker daemon is not running"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Function to test a single Go version
test_go_version() {
local go_version="$1"
local container_name="go-toml-test-${go_version}"
local result_file="${OUTPUT_DIR}/go-${go_version}.txt"
local dockerfile_content
log "Testing Go $go_version..."
# Create a temporary Dockerfile for this version
# For Go versions < 1.21, we need to update go.mod to match the Go version
local needs_go_mod_update=false
if [[ $(echo "$go_version 1.21" | tr ' ' '\n' | sort -V | head -n1) == "$go_version" && "$go_version" != "1.21" ]]; then
needs_go_mod_update=true
fi
dockerfile_content="FROM golang:${go_version}-alpine
# Install git (required for go mod)
RUN apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy source code
COPY . ."
# Add go.mod update step for older Go versions
if [[ "$needs_go_mod_update" == true ]]; then
dockerfile_content="$dockerfile_content
# Update go.mod to match Go version (required for Go < 1.21)
RUN if [ -f go.mod ]; then sed -i 's/^go [0-9]\\+\\.[0-9]\\+\\(\\.[0-9]\\+\\)\\?/go $go_version/' go.mod; fi
# Note: Go versions < 1.21 may fail due to missing standard library packages (e.g., slices)
# This is expected for projects that use Go 1.21+ features"
fi
dockerfile_content="$dockerfile_content
# Run tests
CMD [\"sh\", \"-c\", \"go version && echo '--- Running go test ./... ---' && go test ./...\"]"
# Create temporary directory for this test
local temp_dir
temp_dir=$(mktemp -d)
# Copy source to temp directory (excluding test results and git)
rsync -a --exclude="$OUTPUT_DIR" --exclude=".git" --exclude="*.test" . "$temp_dir/"
# Create Dockerfile in temp directory
echo "$dockerfile_content" > "$temp_dir/Dockerfile"
# Build and run container
local exit_code=0
local output
if $VERBOSE; then
log "Building Docker image for Go $go_version..."
fi
# Capture both stdout and stderr, and the exit code
if output=$(cd "$temp_dir" && timeout "$DOCKER_TIMEOUT" docker build -t "$container_name" . 2>&1 && \
timeout "$DOCKER_TIMEOUT" docker run --rm "$container_name" 2>&1); then
log_success "Go $go_version: PASSED"
echo "PASSED" > "${result_file}.status"
else
exit_code=$?
log_error "Go $go_version: FAILED (exit code: $exit_code)"
echo "FAILED" > "${result_file}.status"
fi
# Save full output
echo "$output" > "$result_file"
# Clean up
docker rmi "$container_name" &> /dev/null || true
rm -rf "$temp_dir"
if $VERBOSE; then
echo "--- Go $go_version output ---"
echo "$output"
echo "--- End Go $go_version output ---"
fi
return $exit_code
}
# Function to run tests in parallel
run_parallel() {
local pids=()
local failed_versions=()
log "Starting parallel tests for ${#GO_VERSIONS[@]} Go versions..."
# Start all tests in background
for version in "${GO_VERSIONS[@]}"; do
test_go_version "$version" &
pids+=($!)
done
# Wait for all tests to complete
for i in "${!pids[@]}"; do
local pid=${pids[$i]}
local version=${GO_VERSIONS[$i]}
if ! wait $pid; then
failed_versions+=("$version")
fi
done
return ${#failed_versions[@]}
}
# Function to run tests sequentially
run_sequential() {
local failed_versions=()
log "Starting sequential tests for ${#GO_VERSIONS[@]} Go versions..."
for version in "${GO_VERSIONS[@]}"; do
if ! test_go_version "$version"; then
failed_versions+=("$version")
fi
done
return ${#failed_versions[@]}
}
# Main execution
main() {
local start_time
start_time=$(date +%s)
log "Starting Go version compatibility tests..."
log "Testing versions: ${GO_VERSIONS[*]}"
log "Output directory: $OUTPUT_DIR"
log "Parallel execution: $PARALLEL"
local failed_count
if $PARALLEL; then
run_parallel
failed_count=$?
else
run_sequential
failed_count=$?
fi
local end_time
end_time=$(date +%s)
local duration=$((end_time - start_time))
# Collect results for display
local passed_versions=()
local failed_versions=()
local unknown_versions=()
local passed_count=0
for version in "${GO_VERSIONS[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
if [[ -f "$status_file" ]]; then
local status
status=$(cat "$status_file")
if [[ "$status" == "PASSED" ]]; then
passed_versions+=("$version")
((passed_count++))
else
failed_versions+=("$version")
fi
else
unknown_versions+=("$version")
fi
done
# Generate summary report
local summary_file="${OUTPUT_DIR}/summary.txt"
{
echo "Go Version Compatibility Test Summary"
echo "====================================="
echo "Date: $(date)"
echo "Duration: ${duration}s"
echo "Parallel: $PARALLEL"
echo ""
echo "Results:"
for version in "${GO_VERSIONS[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
if [[ -f "$status_file" ]]; then
local status
status=$(cat "$status_file")
if [[ "$status" == "PASSED" ]]; then
echo " Go $version: ✓ PASSED"
else
echo " Go $version: ✗ FAILED"
fi
else
echo " Go $version: ? UNKNOWN (no status file)"
fi
done
echo ""
echo "Summary: $passed_count/${#GO_VERSIONS[@]} versions passed"
if [[ $failed_count -gt 0 ]]; then
echo ""
echo "Failed versions details:"
for version in "${failed_versions[@]}"; do
echo ""
echo "--- Go $version (FAILED) ---"
local result_file="${OUTPUT_DIR}/go-${version}.txt"
if [[ -f "$result_file" ]]; then
tail -n 30 "$result_file"
fi
done
fi
} > "$summary_file"
# Find lowest continuous supported version and check recent versions
local lowest_continuous_version=""
local recent_versions_failed=false
# Sort versions to ensure proper order
local sorted_versions=()
for version in "${GO_VERSIONS[@]}"; do
sorted_versions+=("$version")
done
# Sort versions numerically (1.11, 1.12, ..., 1.25)
IFS=$'\n' sorted_versions=($(sort -V <<< "${sorted_versions[*]}"))
# Find lowest continuous supported version (all versions from this point onwards pass)
for version in "${sorted_versions[@]}"; do
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
local all_subsequent_pass=true
# Check if this version and all subsequent versions pass
local found_current=false
for check_version in "${sorted_versions[@]}"; do
if [[ "$check_version" == "$version" ]]; then
found_current=true
fi
if [[ "$found_current" == true ]]; then
local check_status_file="${OUTPUT_DIR}/go-${check_version}.txt.status"
if [[ -f "$check_status_file" ]]; then
local status
status=$(cat "$check_status_file")
if [[ "$status" != "PASSED" ]]; then
all_subsequent_pass=false
break
fi
else
all_subsequent_pass=false
break
fi
fi
done
if [[ "$all_subsequent_pass" == true ]]; then
lowest_continuous_version="$version"
break
fi
done
# Check if the two most recent versions failed
local num_versions=${#sorted_versions[@]}
if [[ $num_versions -ge 2 ]]; then
local second_recent="${sorted_versions[$((num_versions-2))]}"
local most_recent="${sorted_versions[$((num_versions-1))]}"
local second_recent_status_file="${OUTPUT_DIR}/go-${second_recent}.txt.status"
local most_recent_status_file="${OUTPUT_DIR}/go-${most_recent}.txt.status"
local second_recent_failed=false
local most_recent_failed=false
if [[ -f "$second_recent_status_file" ]]; then
local status
status=$(cat "$second_recent_status_file")
if [[ "$status" != "PASSED" ]]; then
second_recent_failed=true
fi
else
second_recent_failed=true
fi
if [[ -f "$most_recent_status_file" ]]; then
local status
status=$(cat "$most_recent_status_file")
if [[ "$status" != "PASSED" ]]; then
most_recent_failed=true
fi
else
most_recent_failed=true
fi
if [[ "$second_recent_failed" == true || "$most_recent_failed" == true ]]; then
recent_versions_failed=true
fi
elif [[ $num_versions -eq 1 ]]; then
# Only one version tested, check if it's the most recent and failed
local only_version="${sorted_versions[0]}"
local only_status_file="${OUTPUT_DIR}/go-${only_version}.txt.status"
if [[ -f "$only_status_file" ]]; then
local status
status=$(cat "$only_status_file")
if [[ "$status" != "PASSED" ]]; then
recent_versions_failed=true
fi
else
recent_versions_failed=true
fi
fi
# Display summary
echo ""
log "Test completed in ${duration}s"
log "Summary report: $summary_file"
echo ""
echo "========================================"
echo " FINAL RESULTS"
echo "========================================"
echo ""
# Display passed versions
if [[ ${#passed_versions[@]} -gt 0 ]]; then
log_success "PASSED (${#passed_versions[@]}/${#GO_VERSIONS[@]}):"
# Sort passed versions for display
local sorted_passed=()
for version in "${sorted_versions[@]}"; do
for passed_version in "${passed_versions[@]}"; do
if [[ "$version" == "$passed_version" ]]; then
sorted_passed+=("$version")
break
fi
done
done
for version in "${sorted_passed[@]}"; do
echo -e " ${GREEN}✓${NC} Go $version"
done
echo ""
fi
# Display failed versions
if [[ ${#failed_versions[@]} -gt 0 ]]; then
log_error "FAILED (${#failed_versions[@]}/${#GO_VERSIONS[@]}):"
# Sort failed versions for display
local sorted_failed=()
for version in "${sorted_versions[@]}"; do
for failed_version in "${failed_versions[@]}"; do
if [[ "$version" == "$failed_version" ]]; then
sorted_failed+=("$version")
break
fi
done
done
for version in "${sorted_failed[@]}"; do
echo -e " ${RED}✗${NC} Go $version"
done
echo ""
# Show failure details
echo "========================================"
echo " FAILURE DETAILS"
echo "========================================"
echo ""
for version in "${sorted_failed[@]}"; do
echo -e "${RED}--- Go $version FAILURE LOGS (last 30 lines) ---${NC}"
local result_file="${OUTPUT_DIR}/go-${version}.txt"
if [[ -f "$result_file" ]]; then
tail -n 30 "$result_file" | sed 's/^/ /'
else
echo " No log file found: $result_file"
fi
echo ""
done
fi
# Display unknown versions
if [[ ${#unknown_versions[@]} -gt 0 ]]; then
log_warning "UNKNOWN (${#unknown_versions[@]}/${#GO_VERSIONS[@]}):"
for version in "${unknown_versions[@]}"; do
echo -e " ${YELLOW}?${NC} Go $version (no status file)"
done
echo ""
fi
echo "========================================"
echo " COMPATIBILITY SUMMARY"
echo "========================================"
echo ""
if [[ -n "$lowest_continuous_version" ]]; then
log_success "Lowest continuous supported version: Go $lowest_continuous_version"
echo " (All versions from Go $lowest_continuous_version onwards pass)"
else
log_error "No continuous version support found"
echo " (No version has all subsequent versions passing)"
fi
echo ""
echo "========================================"
echo "Full detailed logs available in: $OUTPUT_DIR"
echo "========================================"
# Determine exit code based on recent versions
if [[ "$recent_versions_failed" == true ]]; then
log_error "OVERALL RESULT: Recent Go versions failed - this needs attention!"
if [[ -n "$lowest_continuous_version" ]]; then
echo "Note: Continuous support starts from Go $lowest_continuous_version"
fi
exit 1
else
log_success "OVERALL RESULT: Recent Go versions pass - compatibility looks good!"
if [[ -n "$lowest_continuous_version" ]]; then
echo "Continuous support starts from Go $lowest_continuous_version"
fi
exit 0
fi
}
# Trap to clean up on exit
cleanup() {
# Kill any remaining background processes
jobs -p | xargs -r kill 2>/dev/null || true
# Clean up any remaining Docker containers
docker ps -q --filter "name=go-toml-test-" | xargs -r docker stop 2>/dev/null || true
docker images -q --filter "reference=go-toml-test-*" | xargs -r docker rmi 2>/dev/null || true
}
trap cleanup EXIT
# Run main function
main
+246
View File
@@ -0,0 +1,246 @@
;; This document describes TOML's syntax, using the ABNF format (defined in
;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt).
;;
;; All valid TOML documents will match this description, however certain
;; invalid documents would need to be rejected as per the semantics described
;; in the supporting text description.
;; It is possible to try this grammar interactively, using instaparse.
;; http://instaparse.mojombo.com/
;;
;; To do so, in the lower right, click on Options and change `:input-format` to
;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
;; (above the options). Then you can type or paste a sample TOML document into
;; the beige box on the left. Tada!
;; Overall Structure
toml = expression *( newline expression )
expression = ws [ comment ]
expression =/ ws keyval ws [ comment ]
expression =/ ws table ws [ comment ]
;; Whitespace
ws = *wschar
wschar = %x20 ; Space
wschar =/ %x09 ; Horizontal tab
;; Newline
newline = %x0A ; LF
newline =/ %x0D.0A ; CRLF
;; Comment
comment-start-symbol = %x23 ; #
non-ascii = %x80-D7FF / %xE000-10FFFF
non-eol = %x09 / %x20-7E / non-ascii
comment = comment-start-symbol *non-eol
;; Key-Value pairs
keyval = key keyval-sep val
key = simple-key / dotted-key
simple-key = quoted-key / unquoted-key
unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
quoted-key = basic-string / literal-string
dotted-key = simple-key 1*( dot-sep simple-key )
dot-sep = ws %x2E ws ; . Period
keyval-sep = ws %x3D ws ; =
val = string / boolean / array / inline-table / date-time / float / integer
;; String
string = ml-basic-string / basic-string / ml-literal-string / literal-string
;; Basic String
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
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 =/ %x78 2HEXDIG ; xHH U+00HH
escape-seq-char =/ %x75 4HEXDIG ; uHHHH U+HHHH
escape-seq-char =/ %x55 8HEXDIG ; UHHHHHHHH U+HHHHHHHH
;; Multiline Basic String
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 )
;; Literal String
literal-string = apostrophe *literal-char apostrophe
apostrophe = %x27 ; ' apostrophe
literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
;; Multiline Literal String
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
;; Integer
integer = dec-int / hex-int / oct-int / bin-int
minus = %x2D ; -
plus = %x2B ; +
underscore = %x5F ; _
digit1-9 = %x31-39 ; 1-9
digit0-7 = %x30-37 ; 0-7
digit0-1 = %x30-31 ; 0-1
hex-prefix = %x30.78 ; 0x
oct-prefix = %x30.6F ; 0o
bin-prefix = %x30.62 ; 0b
dec-int = [ minus / plus ] unsigned-dec-int
unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
;; Float
float = float-int-part ( exp / frac [ exp ] )
float =/ special-float
float-int-part = dec-int
frac = decimal-point zero-prefixable-int
decimal-point = %x2E ; .
zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
exp = "e" float-exp-part
float-exp-part = [ minus / plus ] zero-prefixable-int
special-float = [ minus / plus ] ( inf / nan )
inf = %x69.6e.66 ; inf
nan = %x6e.61.6e ; nan
;; Boolean
boolean = true / false
true = %x74.72.75.65 ; true
false = %x66.61.6C.73.65 ; false
;; Date and Time (as defined in RFC 3339)
date-time = offset-date-time / local-date-time / local-date / local-time
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
time-delim = "T" / %x20 ; T, t, or space
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
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 ] ]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
;; Offset Date-Time
offset-date-time = full-date time-delim full-time
;; Local Date-Time
local-date-time = full-date time-delim partial-time
;; Local Date
local-date = full-date
;; Local Time
local-time = partial-time
;; Array
array = array-open [ array-values ] ws-comment-newline array-close
array-open = %x5B ; [
array-close = %x5D ; ]
array-values = ws-comment-newline val ws-comment-newline array-sep array-values
array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
array-sep = %x2C ; , Comma
ws-comment-newline = *( wschar / [ comment ] newline )
;; Table
table = std-table / array-table
;; Standard Table
std-table = std-table-open key std-table-close
std-table-open = %x5B ws ; [ Left square bracket
std-table-close = ws %x5D ; ] Right square bracket
;; Inline Table
inline-table = inline-table-open [ inline-table-keyvals ] ws-comment-newline inline-table-close
inline-table-open = %x7B ; {
inline-table-close = %x7D ; }
inline-table-sep = %x2C ; , Comma
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
array-table = array-table-open key array-table-close
array-table-open = %x5B.5B ws ; [[ Double left square bracket
array-table-close = ws %x5D.5D ; ]] Double right square bracket
;; Built-in ABNF terms, reproduced here for clarity
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
DIGIT = %x30-39 ; 0-9
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+24
View File
@@ -0,0 +1,24 @@
package toml
import (
"encoding"
"reflect"
"time"
)
// 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{})
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
+124
View File
@@ -0,0 +1,124 @@
package unstable
import (
"errors"
"fmt"
)
// Iterator over a sequence of nodes.
//
// Starts uninitialized, you need to call Next() first.
//
// For example:
//
// it := n.Children()
// for it.Next() {
// n := it.Node()
// // do something with n
// }
type Iterator struct {
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a node, false
// otherwise.
func (c *Iterator) Next() bool {
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
}
return c.node.Valid()
}
// IsLast returns true if the current node of the iterator is the last
// one. Subsequent calls to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next == 0
}
// Node returns a pointer to the node pointed at by the iterator.
func (c *Iterator) Node() *Node {
return c.node
}
// Node in a TOML expression AST.
//
// Depending on Kind, its sequence of children should be interpreted
// differently.
//
// - Array have one child per element in the array.
// - InlineTable have one child per key-value in the table (each of kind
// InlineTable).
// - KeyValue have at least two children. The first one is the value. The
// rest make a potentially dotted key.
// - Table and ArrayTable's children represent a dotted key (same as
// KeyValue, but without the first node being the value).
//
// When relevant, Raw describes the range of bytes this node is referring to in
// the input document. Use Parser.Raw() to retrieve the actual bytes.
type Node struct {
Kind Kind
Raw Range // Raw bytes from the input.
Data []byte // Node value (either allocated or referencing the input).
// References to other nodes, as 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 {
return nil
}
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 there is no
// child node.
func (n *Node) Child() *Node {
if n.child == 0 {
return nil
}
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 && 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
// would return just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(errors.New("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil. Panics if not called on a KeyValue node,
// or if the Children are malformed.
func (n *Node) Value() *Node {
return n.Child()
}
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
}
+21
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)
}
}
+3
View File
@@ -0,0 +1,3 @@
// Package unstable provides APIs that do not meet the backward compatibility
// guarantees yet.
package unstable
+83
View File
@@ -0,0 +1,83 @@
package unstable
import "fmt"
// Kind represents the type of TOML structure contained in a given Node.
type Kind int
const (
// Invalid represents an invalid meta node.
Invalid Kind = iota
// Comment represents a comment meta node.
Comment
// Key represents a key meta node.
Key
// Table represents a top-level table.
Table
// ArrayTable represents a top-level array table.
ArrayTable
// KeyValue represents a top-level key value.
KeyValue
// Array represents an array container value.
Array
// InlineTable represents an inline table container value.
InlineTable
// String represents a string value.
String
// Bool represents a boolean value.
Bool
// Float represents a floating point value.
Float
// Integer represents an integer value.
Integer
// LocalDate represents a local date value.
LocalDate
// LocalTime represents a local time value.
LocalTime
// LocalDateTime represents a local date/time value.
LocalDateTime
// DateTime represents a data/time value.
DateTime
)
// String implementation of fmt.Stringer.
func (k Kind) String() string {
switch k {
case Invalid:
return "Invalid"
case Comment:
return "Comment"
case Key:
return "Key"
case Table:
return "Table"
case ArrayTable:
return "ArrayTable"
case KeyValue:
return "KeyValue"
case Array:
return "Array"
case InlineTable:
return "InlineTable"
case String:
return "String"
case Bool:
return "Bool"
case Float:
return "Float"
case Integer:
return "Integer"
case LocalDate:
return "LocalDate"
case LocalTime:
return "LocalTime"
case LocalDateTime:
return "LocalDateTime"
case DateTime:
return "DateTime"
}
panic(fmt.Errorf("Kind.String() not implemented for kind %d", int(k)))
}
+29
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
+43
View File
@@ -0,0 +1,43 @@
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.
//
// 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 both Unmarshaler and
// Marshaler and can be used to delay TOML decoding or capture raw content,
// similar to json.RawMessage.
//
// Decoding (requires Decoder.EnableUnmarshalerInterface) captures the raw TOML
// bytes for the target without decoding them:
//
// type Config struct {
// Plugin RawMessage `toml:"plugin"`
// }
//
// 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], data...)
return nil
}