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:
+2
@@ -0,0 +1,2 @@
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
@@ -0,0 +1,4 @@
|
||||
* text=auto
|
||||
|
||||
benchmark/benchmark.toml text eol=lf
|
||||
testdata/** text eol=lf
|
||||
@@ -0,0 +1,8 @@
|
||||
test_program/test_program_bin
|
||||
fuzz/
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
cmd/tomltestgen/tomltestgen
|
||||
dist
|
||||
tests/
|
||||
test-results
|
||||
+76
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
[](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
@@ -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
@@ -0,0 +1 @@
|
||||
github.com/pelletier/go-toml/v2: CAPABILITY_REFLECT, CAPABILITY_UNANALYZED, CAPABILITY_UNSAFE_POINTER
|
||||
+101
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package toml is a library to read and write TOML documents.
|
||||
package toml
|
||||
+233
@@ -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")
|
||||
}
|
||||
+37
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package tracker provides functions for keeping track of AST nodes.
|
||||
package tracker
|
||||
+121
@@ -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
|
||||
}
|
||||
+1697
File diff suppressed because it is too large
Load Diff
+108
@@ -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
@@ -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
@@ -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
@@ -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("")
|
||||
)
|
||||
+2396
File diff suppressed because it is too large
Load Diff
+124
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
// Package unstable provides APIs that do not meet the backward compatibility
|
||||
// guarantees yet.
|
||||
package unstable
|
||||
+83
@@ -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
@@ -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
|
||||
}
|
||||
+1686
File diff suppressed because it is too large
Load Diff
+43
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user