Compare commits

...

18 Commits

Author SHA1 Message Date
Claude 6f19f855f1 Fix leap second overflow in datetime parsing (#1015)
Normalize leap seconds (second=60) to second=59 before passing to
time.Date() to prevent year overflow. When Go's time.Date() receives
second=60, it normalizes the time by adding 1 minute, which can cause
dates like 9999-12-31 23:59:60 to overflow to year 10000 - outside
the valid TOML date range (0000-9999).

This fix ensures that timestamps with leap seconds can be successfully
round-tripped (parsed and re-serialized) without causing parsing errors.

Fixes OSS-Fuzz issue 472183443
2026-01-04 02:23:40 +00:00
Alexander Hecke 3cf1eb2312 improve Unmarshaling documentation (#1016) 2026-01-03 21:12:35 -05:00
Nathan Baulch 2af3554f90 Update toml-test to v1.6.0 (#1007) 2026-01-03 20:45:06 -05:00
dependabot[bot] 180c6ba2ba build(deps): bump actions/setup-go from 5 to 6 (#1002)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 20:43:53 -05:00
dependabot[bot] dafc4173ef build(deps): bump github/codeql-action from 3 to 4 (#1006)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 20:43:43 -05:00
dependabot[bot] f1a83be671 build(deps): bump actions/upload-artifact from 4 to 6 (#1011)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 20:43:33 -05:00
dependabot[bot] 5aeb70b3f0 build(deps): bump actions/checkout from 5 to 6 (#1010)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 20:43:20 -05:00
W. Michael Petullo 8384a5683c Use constant format strings with Printf-like functions (#1013)
Recent versions of Go object to the use of non-constant variables a
format strings. This commit fixes errors like this:

cli.go:26:47: non-constant format string in call to fmt.Fprintf

Signed-off-by: W. Michael Petullo <mike@flyn.org>
2026-01-03 20:42:58 -05:00
Étienne BERSAC 4369957cb4 Unwrap strict errors (#1012) 2025-12-21 16:20:24 +01:00
dependabot[bot] a0e8464967 build(deps): bump actions/checkout from 4 to 5 (#1001)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 09:53:08 +02:00
Nathan Baulch c57d0d559f Add omitzero tag support (#998) 2025-08-25 08:06:48 +02:00
Thomas Pelletier 644602b845 Script to test all versions of go (#1000) 2025-08-24 12:40:29 +02:00
Nathan Baulch 36df8eef6e General cleanup (#999) 2025-08-24 12:18:46 +02:00
Thomas Pelletier 18a2148713 Handle array table into an empty slice (#997)
Fix #995
2025-08-21 12:05:41 +02:00
Thomas Pelletier bc9958322f Add missing UnmarshalTOML call (#996)
Fixes #994.
2025-08-21 10:39:23 +02:00
Dustin Spicuzza 6d56ac8027 marshal: don't escape quotes unnecessarily (#991)
Only 3 consecutive quotation marks need to be quoted. We choose to quote
all quotation marks in a sequence if there are 3 or more consecutive
present.

Fixes #990

---------

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
2025-08-21 08:19:16 +02:00
dependabot[bot] 098464b61b build(deps): bump actions/checkout from 4 to 5 (#993)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 08:10:55 +02:00
Oleksandr Redko 85e2448ce5 refactor: Simplify t.Fatalf (#984) 2025-05-10 15:14:34 -04:00
31 changed files with 1725 additions and 269 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
+5 -5
View File
@@ -35,11 +35,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,10 +47,10 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+2 -2
View File
@@ -9,11 +9,11 @@ jobs:
runs-on: "ubuntu-latest"
name: report
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
- name: Run tests with coverage
+36
View File
@@ -0,0 +1,36 @@
name: Go Versions Compatibility Test
on:
workflow_dispatch:
inputs:
go_versions:
description: 'Go versions to test (space-separated, e.g., "1.21 1.22 1.23")'
required: false
default: ''
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Run Go versions compatibility test
run: |
VERSIONS="${{ github.event.inputs.go_versions }}"
./test-go-versions.sh --output ./test-results $VERSIONS
- name: Upload test results
uses: actions/upload-artifact@v6
with:
name: go-versions-test-results
path: |
test-results/
retention-days: 30
+2 -2
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
- name: Login to GitHub Container Registry
+2 -2
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup go ${{ matrix.go }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Run unit tests
+1
View File
@@ -5,3 +5,4 @@ cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
dist
tests/
test-results
+51 -9
View File
@@ -33,7 +33,7 @@ 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
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.
@@ -92,6 +92,48 @@ 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
@@ -111,7 +153,7 @@ code lowers the coverage.
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
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:
@@ -168,13 +210,13 @@ Checklist:
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
```
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.
+61 -1
View File
@@ -107,7 +107,11 @@ type MyConfig struct {
### Unmarshaling
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
content. For example:
content.
Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_.
For example:
```go
doc := `
@@ -133,6 +137,62 @@ fmt.Println("tags:", cfg.Tags)
[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
+2 -2
View File
@@ -3,7 +3,7 @@ package benchmark_test
import (
"compress/gzip"
"encoding/json"
"io/ioutil"
"io"
"os"
"path/filepath"
"testing"
@@ -74,7 +74,7 @@ func fixture(tb testing.TB, path string) []byte {
gz, err := gzip.NewReader(f)
assert.NoError(tb, err)
buf, err := ioutil.ReadAll(gz)
buf, err := io.ReadAll(gz)
assert.NoError(tb, err)
return buf
}
+4 -4
View File
@@ -2,7 +2,7 @@ package benchmark_test
import (
"bytes"
"io/ioutil"
"os"
"testing"
"time"
@@ -59,7 +59,7 @@ func BenchmarkUnmarshal(b *testing.B) {
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
bytes, err := os.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
@@ -165,7 +165,7 @@ func BenchmarkMarshal(b *testing.B) {
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
bytes, err := os.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
@@ -344,7 +344,7 @@ type benchmarkDoc struct {
}
func TestUnmarshalReferenceFile(t *testing.T) {
bytes, err := ioutil.ReadFile("benchmark.toml")
bytes, err := os.ReadFile("benchmark.toml")
assert.NoError(t, err)
d := benchmarkDoc{}
err = toml.Unmarshal(bytes, &d)
+2
View File
@@ -106,6 +106,7 @@ func main() {
for _, f := range dirContent {
filename := strings.TrimPrefix(f, "tests/valid/")
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
name = strings.ReplaceAll(name, ".", "_")
log.Printf("> [%s] %s\n", "invalid", name)
@@ -126,6 +127,7 @@ func main() {
for _, f := range dirContent {
filename := strings.TrimPrefix(f, "tests/valid/")
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
name = strings.ReplaceAll(name, ".", "_")
log.Printf("> [%s] %s\n", "valid", name)
+11 -1
View File
@@ -144,13 +144,23 @@ func parseDateTime(b []byte) (time.Time, error) {
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
}
// Normalize leap seconds (second=60) to second=59 to prevent overflow
// when Go's time.Date normalizes the time. This is necessary because
// time.Date(9999, 12, 31, 23, 59, 60, 0, UTC) normalizes to year 10000,
// which is outside the valid TOML date range (0000-9999).
// See: https://github.com/pelletier/go-toml/issues/1015
second := dt.Second
if second == 60 {
second = 59
}
t := time.Date(
dt.Year,
time.Month(dt.Month),
dt.Day,
dt.Hour,
dt.Minute,
dt.Second,
second,
dt.Nanosecond,
zone)
+12 -1
View File
@@ -54,6 +54,17 @@ func (s *StrictMissingError) String() string {
return buf.String()
}
// Unwrap returns wrapped decode errors
//
// Implements errors.Join() interface.
func (s *StrictMissingError) Unwrap() []error {
var errs []error
for i := range s.Errors {
errs = append(errs, &s.Errors[i])
}
return errs
}
type Key []string
// Error returns the error message contained in the DecodeError.
@@ -78,7 +89,7 @@ func (e *DecodeError) Key() Key {
return e.key
}
// decodeErrorFromHighlight creates a DecodeError referencing a highlighted
// wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document.
//
// highlight needs to be a sub-slice of document, or this function panics.
+15
View File
@@ -205,6 +205,21 @@ func TestDecodeError_Accessors(t *testing.T) {
assert.Equal(t, "bar", e.String())
}
func TestStrictErrorUnwrap(t *testing.T) {
fo := bytes.NewBufferString(`
Missing = 1
OtherMissing = 1
`)
var out struct{}
err := NewDecoder(fo).DisallowUnknownFields().Decode(&out)
assert.Error(t, err)
strictErr := &StrictMissingError{}
assert.True(t, errors.As(err, &strictErr))
assert.Equal(t, 2, len(strictErr.Unwrap()))
}
func ExampleDecodeError() {
doc := `name = 123__456`
+2 -2
View File
@@ -1,7 +1,7 @@
package toml_test
import (
"io/ioutil"
"os"
"strings"
"testing"
@@ -10,7 +10,7 @@ import (
)
func FuzzUnmarshal(f *testing.F) {
file, err := ioutil.ReadFile("benchmark/benchmark.toml")
file, err := os.ReadFile("benchmark/benchmark.toml")
if err != nil {
panic(err)
}
+2 -2
View File
@@ -79,7 +79,7 @@ func Zero[T any](t testing.TB, value T, msgAndArgs ...any) {
}
t.Helper()
msg := formatMsgAndArgs("Expected zero value but got:", msgAndArgs...)
t.Fatalf("%s\n%s", msg, fmt.Sprintf("%v", value))
t.Fatalf("%s\n%v", msg, value)
}
func NotZero[T any](t testing.TB, value T, msgAndArgs ...any) {
@@ -92,7 +92,7 @@ func NotZero[T any](t testing.TB, value T, msgAndArgs ...any) {
}
t.Helper()
msg := formatMsgAndArgs("Unexpected zero value:", msgAndArgs...)
t.Fatalf("%s\n%s", msg, fmt.Sprintf("%v", value))
t.Fatalf("%s\n%v", msg, value)
}
func formatMsgAndArgs(msg string, args ...any) string {
+3 -4
View File
@@ -6,7 +6,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml/v2"
@@ -23,7 +22,7 @@ type Program struct {
}
func (p *Program) Execute() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, p.Usage) }
flag.Usage = func() { fmt.Fprint(os.Stderr, p.Usage) }
flag.Parse()
os.Exit(p.main(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
@@ -72,7 +71,7 @@ func (p *Program) runAllFilesInPlace(files []string) error {
}
func (p *Program) runFileInPlace(path string) error {
in, err := ioutil.ReadFile(path)
in, err := os.ReadFile(path)
if err != nil {
return err
}
@@ -84,5 +83,5 @@ func (p *Program) runFileInPlace(path string) error {
return err
}
return ioutil.WriteFile(path, out.Bytes(), 0600)
return os.WriteFile(path, out.Bytes(), 0600)
}
+10 -11
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
@@ -63,7 +62,7 @@ func TestProcessMainStdinDecodeErr(t *testing.T) {
}
func TestProcessMainFileExists(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "example")
tmpfile, err := os.CreateTemp("", "example")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = tmpfile.Write([]byte(`some data`))
@@ -95,16 +94,16 @@ func TestProcessMainFileDoesNotExist(t *testing.T) {
}
func TestProcessMainFilesInPlace(t *testing.T) {
dir, err := ioutil.TempDir("", "")
dir, err := os.MkdirTemp("", "")
assert.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
path2 := path.Join(dir, "file2")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
err = os.WriteFile(path1, []byte("content 1"), 0600)
assert.NoError(t, err)
err = ioutil.WriteFile(path2, []byte("content 2"), 0600)
err = os.WriteFile(path2, []byte("content 2"), 0600)
assert.NoError(t, err)
p := Program{
@@ -116,11 +115,11 @@ func TestProcessMainFilesInPlace(t *testing.T) {
assert.Equal(t, 0, exit)
v1, err := ioutil.ReadFile(path1)
v1, err := os.ReadFile(path1)
assert.NoError(t, err)
assert.Equal(t, "1", string(v1))
v2, err := ioutil.ReadFile(path2)
v2, err := os.ReadFile(path2)
assert.NoError(t, err)
assert.Equal(t, "2", string(v2))
}
@@ -137,13 +136,13 @@ func TestProcessMainFilesInPlaceErrRead(t *testing.T) {
}
func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
dir, err := ioutil.TempDir("", "")
dir, err := os.MkdirTemp("", "")
assert.NoError(t, err)
defer os.RemoveAll(dir)
path1 := path.Join(dir, "file1")
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
err = os.WriteFile(path1, []byte("content 1"), 0600)
assert.NoError(t, err)
p := Program{
@@ -155,13 +154,13 @@ func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
assert.Equal(t, -1, exit)
v1, err := ioutil.ReadFile(path1)
v1, err := os.ReadFile(path1)
assert.NoError(t, err)
assert.Equal(t, "content 1", string(v1))
}
func dummyFileFn(r io.Reader, w io.Writer) error {
b, err := ioutil.ReadAll(r)
b, err := io.ReadAll(r)
if err != nil {
return err
}
@@ -2283,7 +2283,7 @@ func (c *Custom) UnmarshalTOML(v interface{}) error {
return nil
}
func TestGithubIssue431(t *testing.T) {
func TestGitHubIssue431(t *testing.T) {
doc := `key = "value"`
var c Config
if err := toml.Unmarshal([]byte(doc), &c); err != nil {
@@ -2321,7 +2321,7 @@ type config437 struct {
} `toml:"HTTP"`
}
func TestGithubIssue437(t *testing.T) {
func TestGitHubIssue437(t *testing.T) {
t.Skipf("unmarshalTOML not implemented")
src := `
[HTTP]
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/pelletier/go-toml/v2"
)
// Marshal is a helpfer function for calling toml.Marshal
// Marshal is a helper function for calling toml.Marshal
//
// Only needed to avoid package import loops.
func Marshal(v interface{}) ([]byte, error) {
+7 -1
View File
@@ -94,7 +94,13 @@ type LocalDateTime struct {
// 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)
// Normalize leap seconds (second=60) to second=59 to prevent overflow
// when Go's time.Date normalizes the time.
second := d.Second
if second == 60 {
second = 59
}
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, second, d.Nanosecond, zone)
}
// String returns RFC 3339 representation of d.
+37 -3
View File
@@ -161,6 +161,8 @@ func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder {
//
// The "omitempty" option prevents empty values or groups from being emitted.
//
// The "omitzero" option prevents zero values or groups from being emitted.
//
// The "commented" option prefixes the value and all its children with a comment
// symbol.
//
@@ -196,6 +198,7 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct {
multiline bool
omitempty bool
omitzero bool
commented bool
comment string
}
@@ -384,6 +387,10 @@ func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
return options.omitempty && isEmptyValue(v)
}
func shouldOmitZero(options valueOptions, v reflect.Value) bool {
return options.omitzero && v.IsZero()
}
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error
@@ -517,12 +524,26 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
del = 0x7f
)
for _, r := range []byte(v) {
bv := []byte(v)
for i := 0; i < len(bv); i++ {
r := bv[i]
switch r {
case '\\':
b = append(b, `\\`...)
case '"':
b = append(b, `\"`...)
if multiline {
// Quotation marks do not need to be quoted in multiline strings unless
// it contains 3 consecutive. If 3+ quotes appear, quote all of them
// because it's visually better
if i+2 > len(bv) || bv[i+1] != '"' || bv[i+2] != '"' {
b = append(b, r)
} else {
b = append(b, `\"\"\"`...)
i += 2
}
} else {
b = append(b, `\"`...)
}
case '\b':
b = append(b, `\b`...)
case '\f':
@@ -760,6 +781,7 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
options := valueOptions{
multiline: opts.multiline,
omitempty: opts.omitempty,
omitzero: opts.omitzero,
commented: opts.commented,
comment: fieldType.Tag.Get("comment"),
}
@@ -820,6 +842,7 @@ type tagOptions struct {
multiline bool
inline bool
omitempty bool
omitzero bool
commented bool
}
@@ -832,7 +855,7 @@ func parseTag(tag string) (string, tagOptions) {
}
raw := tag[idx+1:]
tag = string(tag[:idx])
tag = tag[:idx]
for raw != "" {
var o string
i := strings.Index(raw, ",")
@@ -848,6 +871,8 @@ func parseTag(tag string) (string, tagOptions) {
opts.inline = true
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
case "commented":
opts.commented = true
}
@@ -882,6 +907,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if shouldOmitZero(kv.Options, kv.Value) {
continue
}
hasNonEmptyKV = true
ctx.setKey(kv.Key)
@@ -901,6 +929,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
if shouldOmitEmpty(table.Options, table.Value) {
continue
}
if shouldOmitZero(table.Options, table.Value) {
continue
}
if first {
first = false
if hasNonEmptyKV {
@@ -935,6 +966,9 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
if shouldOmitEmpty(kv.Options, kv.Value) {
continue
}
if shouldOmitZero(kv.Options, kv.Value) {
continue
}
if first {
first = false
+118 -3
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"math/big"
"net/netip"
"reflect"
"strings"
"testing"
@@ -387,6 +388,54 @@ name = 'Alice'
expected: `A = """
hello
world"""
`,
},
{
desc: "multi-line quotation",
v: struct {
A string `toml:",multiline"`
}{
A: "hello\n\"world\"",
},
expected: `A = """
hello
"world""""
`,
},
{
desc: "multi-line triple quotation",
v: struct {
A string `toml:",multiline"`
}{
A: "hello\n\"\"\"world\"",
},
expected: `A = """
hello
\"\"\"world""""
`,
},
{
desc: "multi-line triple quotation",
v: struct {
A string `toml:",multiline"`
}{
A: "hello\n\"world\"\"\"",
},
expected: `A = """
hello
"world\"\"\""""
`,
},
{
desc: "multi-line sextuple quotation",
v: struct {
A string `toml:",multiline"`
}{
A: "hello\n\"\"\"\"\"\"world\"",
},
expected: `A = """
hello
\"\"\"\"\"\"world""""
`,
},
{
@@ -1069,6 +1118,9 @@ func TestEncoderOmitempty(t *testing.T) {
Ptr *string `toml:",omitempty,multiline"`
Iface interface{} `toml:",omitempty,multiline"`
Struct struct{} `toml:",omitempty,multiline"`
Inline struct {
String string `toml:",omitempty,multiline"`
} `toml:",inline"`
}
d := doc{}
@@ -1076,7 +1128,68 @@ func TestEncoderOmitempty(t *testing.T) {
b, err := toml.Marshal(d)
assert.NoError(t, err)
expected := ``
expected := `Inline = {}
`
assert.Equal(t, expected, string(b))
}
func TestEncoderOmitzero(t *testing.T) {
type doc struct {
String string `toml:",omitzero,multiline"`
Bool bool `toml:",omitzero,multiline"`
Int int `toml:",omitzero,multiline"`
Int8 int8 `toml:",omitzero,multiline"`
Int16 int16 `toml:",omitzero,multiline"`
Int32 int32 `toml:",omitzero,multiline"`
Int64 int64 `toml:",omitzero,multiline"`
Uint uint `toml:",omitzero,multiline"`
Uint8 uint8 `toml:",omitzero,multiline"`
Uint16 uint16 `toml:",omitzero,multiline"`
Uint32 uint32 `toml:",omitzero,multiline"`
Uint64 uint64 `toml:",omitzero,multiline"`
Float32 float32 `toml:",omitzero,multiline"`
Float64 float64 `toml:",omitzero,multiline"`
MapNil map[string]string `toml:",omitzero,multiline"`
Slice []string `toml:",omitzero,multiline"`
Ptr *string `toml:",omitzero,multiline"`
Iface interface{} `toml:",omitzero,multiline"`
Struct struct{} `toml:",omitzero,multiline"`
Time time.Time `toml:",omitzero,multiline"`
IP netip.Addr `toml:",omitzero,multiline"`
Inline struct {
String string `toml:",omitzero,multiline"`
} `toml:",inline"`
}
d := doc{}
b, err := toml.Marshal(d)
assert.NoError(t, err)
expected := `Inline = {}
`
assert.Equal(t, expected, string(b))
}
func TestEncoderOmitzeroOpaqueStruct(t *testing.T) {
type doc struct {
Time time.Time `toml:",omitzero"`
IP netip.Addr `toml:",omitzero"`
}
d := doc{
Time: time.Date(2001, 2, 3, 4, 5, 6, 7, time.UTC),
IP: netip.MustParseAddr("192.168.178.35"),
}
b, err := toml.Marshal(d)
assert.NoError(t, err)
expected := `Time = 2001-02-03T04:05:06.000000007Z
IP = '192.168.178.35'
`
assert.Equal(t, expected, string(b))
}
@@ -1278,7 +1391,8 @@ func TestIssue786(t *testing.T) {
config.Custom = []Custom{{Name: "omit", General: General{Randomize: false}}}
config.Custom = append(config.Custom, Custom{Name: "present", General: General{From: "-2d", Randomize: true}})
encoder := toml.NewEncoder(buf)
encoder.Encode(config)
err = encoder.Encode(config)
assert.NoError(t, err)
expected := `# from in graphite-web format, the local TZ is used
from = '-2d'
@@ -1324,7 +1438,8 @@ func TestMarshalIssue888(t *testing.T) {
}
encoder := toml.NewEncoder(buf).SetIndentTables(true)
encoder.Encode(config)
err := encoder.Encode(config)
assert.NoError(t, err)
expected := `# custom config
[[Custom]]
+596
View File
@@ -0,0 +1,596 @@
#!/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.25)
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"
)
# 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.24 1.25 # 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-5])$ ]]; then
log_error "Invalid Go version: $version. Supported versions: 1.11-1.25"
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
+3 -3
View File
@@ -1,5 +1,5 @@
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@master -copy ./tests
//go:generate go run ./cmd/tomltestgen/main.go -o toml_testgen_test.go
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@v1.6.0 -copy ./tests
//go:generate go run ./cmd/tomltestgen/main.go -r v1.6.0 -o toml_testgen_test.go
// This is a support file for toml_testgen_test.go
package toml_test
@@ -52,7 +52,7 @@ func testgenValid(t *testing.T, input string, jsonRef string) {
assert.NoError(t, err)
var actual interface{}
err = json.Unmarshal([]byte(j), &actual)
err = json.Unmarshal(j, &actual)
assert.NoError(t, err)
testsuite.CmpJSON(t, "", ref, actual)
+382 -194
View File
File diff suppressed because it is too large Load Diff
+39 -4
View File
@@ -416,15 +416,39 @@ func (d *decoder) handleArrayTableCollection(key unstable.Iterator, v reflect.Va
return v, nil
case reflect.Slice:
elem := v.Index(v.Len() - 1)
// Create a new element when the slice is empty; otherwise operate on
// the last element.
var (
elem reflect.Value
created bool
)
if v.Len() == 0 {
created = true
elemType := v.Type().Elem()
if elemType.Kind() == reflect.Interface {
elem = makeMapStringInterface()
} else {
elem = reflect.New(elemType).Elem()
}
} else {
elem = v.Index(v.Len() - 1)
}
x, err := d.handleArrayTable(key, elem)
if err != nil || d.skipUntilTable {
return reflect.Value{}, err
}
if x.IsValid() {
elem.Set(x)
if created {
elem = x
} else {
elem.Set(x)
}
}
if created {
return reflect.Append(v, elem), nil
}
return v, err
case reflect.Array:
idx := d.arrayIndex(false, v)
@@ -1010,7 +1034,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error
case reflect.Interface:
r = reflect.ValueOf(i)
default:
return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("integer", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("integer", v.Type()))
}
if !r.Type().AssignableTo(v.Type()) {
@@ -1029,7 +1053,7 @@ func (d *decoder) unmarshalString(value *unstable.Node, v reflect.Value) error {
case reflect.Interface:
v.Set(reflect.ValueOf(string(value.Data)))
default:
return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("string", v.Type()))
return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("string", v.Type()))
}
return nil
@@ -1154,6 +1178,17 @@ func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node
case reflect.Struct:
path, found := structFieldPath(v, string(key.Node().Data))
if !found {
// If no matching struct field is found but the target implements the
// unstable.Unmarshaler interface (and it is enabled), delegate the
// decoding of this value to the custom unmarshaler.
if d.unmarshalerInterface {
if v.CanAddr() && v.Addr().CanInterface() {
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
return reflect.Value{}, outi.UnmarshalTOML(value)
}
}
}
// Otherwise, keep previous behavior and skip until the next table.
d.skipUntilTable = true
break
}
+314 -7
View File
@@ -412,7 +412,7 @@ foo = "bar"`,
assert: func(t *testing.T, test test) {
// Despite the documentation:
// Pointer variable equality is determined based on the equality of the
// referenced values (as opposed to the memory addresses).
// referenced values (as opposed to the memory addresses).
// assert.Equal does not work properly with maps with pointer keys
// https://github.com/stretchr/testify/issues/1143
expected := make(map[unmarshalTextKey]string)
@@ -2977,7 +2977,8 @@ func TestIssue931(t *testing.T) {
Name = 'd'
`)
toml.Unmarshal(b, &its)
err := toml.Unmarshal(b, &its)
assert.NoError(t, err)
assert.Equal(t, items{[]item{{"c"}, {"d"}}}, its)
}
@@ -2998,7 +2999,8 @@ func TestIssue931Interface(t *testing.T) {
Name = 'd'
`)
toml.Unmarshal(b, &its)
err := toml.Unmarshal(b, &its)
assert.NoError(t, err)
assert.Equal(t, items{[]interface{}{item{"Name": "c"}, item{"Name": "d"}}}, its)
}
@@ -3024,7 +3026,8 @@ func TestIssue931SliceInterface(t *testing.T) {
Name = 'd'
`)
toml.Unmarshal(b, &its)
err := toml.Unmarshal(b, &its)
assert.NoError(t, err)
assert.Equal(t, items{[]interface{}{item{"Name": "c"}, item{"Name": "d"}}}, its)
}
@@ -3884,9 +3887,9 @@ func TestUnmarshal_Nil(t *testing.T) {
{
desc: "simplest",
input: `
[foo]
[foo.foo]
`,
[foo]
[foo.foo]
`,
expected: "[foo]\n[foo.foo]\n",
},
}
@@ -3998,3 +4001,307 @@ foo = "bar"`,
})
}
}
type doc994 struct{}
func (d *doc994) UnmarshalTOML(value *unstable.Node) error {
return errors.New("expected-error")
}
func TestIssue994(t *testing.T) {
var _ unstable.Unmarshaler = (*doc994)(nil)
tomlBytes := []byte(`foo = "bar"`)
var d doc994
err := toml.NewDecoder(bytes.NewReader(tomlBytes)).
EnableUnmarshalerInterface().
Decode(&d)
assert.Error(t, err)
if err.Error() != "expected-error" {
t.Fatalf("expected error 'expected-error', got '%s'", err.Error())
}
}
type doc994ok struct {
S string
}
func (d *doc994ok) UnmarshalTOML(value *unstable.Node) error {
d.S = string(value.Data) + " from unmarshaler"
return nil
}
func TestIssue994_OK(t *testing.T) {
var _ unstable.Unmarshaler = (*doc994ok)(nil)
tomlBytes := []byte(`foo = "bar"`)
var d doc994ok
err := toml.NewDecoder(bytes.NewReader(tomlBytes)).
EnableUnmarshalerInterface().
Decode(&d)
assert.NoError(t, err)
assert.Equal(t, "bar from unmarshaler", d.S)
}
func TestIssue995(t *testing.T) {
type AllowList struct {
Description string
Condition string
Commits []string
Paths []string
RegexTarget string
Regexes []string
StopWords []string
}
type Rule struct {
ID string
Description string
Regex string
SecretGroup int
Entropy interface{}
Keywords []string
Path string
Tags []string
AllowList *AllowList
Allowlists []AllowList
}
type GitleaksConfig struct {
Description string
Rules []Rule
Allowlist struct {
Commits []string
Paths []string
RegexTarget string
Regexes []string
StopWords []string
}
}
doc := `
[[allowlists]]
description = "Exception for File "
files = [ '''app/src''']
[[rules.allowlists]]
description = "policies"
regexes = [
'''abc'''
]
`
var cfg GitleaksConfig
err := toml.Unmarshal([]byte(doc), &cfg)
assert.NoError(t, err)
// Ensure no panic and that nested array table was created.
if len(cfg.Rules) == 0 {
t.Fatalf("expected Rules to contain at least one element after unmarshaling nested array table")
}
if len(cfg.Rules[0].Allowlists) != 1 {
t.Fatalf("expected first Rule to have exactly one allowlists entry, got %d", len(cfg.Rules[0].Allowlists))
}
assert.Equal(t, "policies", cfg.Rules[0].Allowlists[0].Description)
assert.Equal(t, []string{"abc"}, cfg.Rules[0].Allowlists[0].Regexes)
}
func TestIssue995_InterfaceSlice_MultiNested(t *testing.T) {
type Root struct {
Rules []interface{}
}
doc := `
[[rules.allowlists]]
description = "a"
[[rules.allowlists]]
description = "b"
`
var r Root
err := toml.Unmarshal([]byte(doc), &r)
assert.NoError(t, err)
if len(r.Rules) != 1 {
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
}
m, ok := r.Rules[0].(map[string]interface{})
if !ok {
t.Fatalf("expected Rules[0] to be a map[string]any, got %T", r.Rules[0])
}
als, ok := m["allowlists"].([]interface{})
if !ok {
t.Fatalf("expected allowlists to be []any, got %T", m["allowlists"])
}
if len(als) != 2 {
t.Fatalf("expected 2 allowlists entries, got %d", len(als))
}
a0, ok := als[0].(map[string]interface{})
if !ok {
t.Fatalf("expected allowlists[0] to be map[string]any, got %T", als[0])
}
a1, ok := als[1].(map[string]interface{})
if !ok {
t.Fatalf("expected allowlists[1] to be map[string]any, got %T", als[1])
}
assert.Equal(t, "a", a0["description"])
assert.Equal(t, "b", a1["description"])
}
func TestIssue995_MultiNestedConcrete(t *testing.T) {
type AllowList struct {
Description string
}
type Rule struct {
Allowlists []AllowList
}
type Root struct {
Rules []Rule
}
doc := `
[[rules.allowlists]]
description = "a"
[[rules.allowlists]]
description = "b"
`
var r Root
err := toml.Unmarshal([]byte(doc), &r)
assert.NoError(t, err)
if len(r.Rules) != 1 {
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
}
assert.Equal(t, 2, len(r.Rules[0].Allowlists))
assert.Equal(t, "a", r.Rules[0].Allowlists[0].Description)
assert.Equal(t, "b", r.Rules[0].Allowlists[1].Description)
}
func TestIssue995_PointerToSlice_Rules(t *testing.T) {
type AllowList struct{ Description string }
type Rule struct{ Allowlists []AllowList }
type Root struct{ Rules *[]Rule }
doc := `
[[rules.allowlists]]
description = "a"
[[rules.allowlists]]
description = "b"
`
var r Root
err := toml.Unmarshal([]byte(doc), &r)
assert.NoError(t, err)
if r.Rules == nil {
t.Fatalf("expected Rules pointer to be initialized")
}
if len(*r.Rules) != 1 {
t.Fatalf("expected one element in Rules, got %d", len(*r.Rules))
}
rule := (*r.Rules)[0]
assert.Equal(t, 2, len(rule.Allowlists))
assert.Equal(t, "a", rule.Allowlists[0].Description)
assert.Equal(t, "b", rule.Allowlists[1].Description)
}
func TestIssue995_SliceNonEmpty_UsesLastElement(t *testing.T) {
type AllowList struct{ Description string }
type Rule struct{ Allowlists []AllowList }
type Root struct{ Rules []Rule }
// Pre-initialize with one Rule; nested array table should populate
// the last element, not create a new one at this level.
var r Root
r.Rules = []Rule{{}}
doc := `
[[rules.allowlists]]
description = "a"
[[rules.allowlists]]
description = "b"
`
err := toml.Unmarshal([]byte(doc), &r)
assert.NoError(t, err)
if len(r.Rules) != 1 {
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
}
assert.Equal(t, 2, len(r.Rules[0].Allowlists))
// Values presence check
got := []string{r.Rules[0].Allowlists[0].Description, r.Rules[0].Allowlists[1].Description}
if !(got[0] == "a" && got[1] == "b") && !(got[0] == "b" && got[1] == "a") {
t.Fatalf("unexpected values in allowlists: %v", got)
}
}
// TestLeapSecondRoundTrip tests that leap seconds (second=60) don't cause
// year overflow issues during round-trip marshaling. This reproduces OSS-Fuzz
// issue 472183443.
func TestLeapSecondRoundTrip(t *testing.T) {
// This is the test case from OSS-Fuzz issue #1015
input := []byte("s=9999-12-31 23:59:60z")
var v interface{}
err := toml.Unmarshal(input, &v)
assert.NoError(t, err)
// Marshal back to TOML
encoded, err := toml.Marshal(v)
assert.NoError(t, err)
// Unmarshal again - this should not fail with year overflow
var v2 interface{}
err = toml.Unmarshal(encoded, &v2)
assert.NoError(t, err)
}
// TestLeapSecondVariants tests various leap second edge cases
func TestLeapSecondVariants(t *testing.T) {
testCases := []struct {
name string
input string
}{
{
name: "leap second with UTC offset datetime",
input: "s=9999-12-31 23:59:60z",
},
{
name: "leap second with positive offset",
input: "s=9999-12-31 23:59:60+00:00",
},
{
name: "leap second with negative offset",
input: "s=9999-12-31 23:59:60-05:00",
},
{
name: "leap second earlier in year",
input: "s=2015-06-30 23:59:60z",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var v interface{}
err := toml.Unmarshal([]byte(tc.input), &v)
assert.NoError(t, err)
// Marshal back to TOML
encoded, err := toml.Marshal(v)
assert.NoError(t, err)
// Unmarshal again - this should not fail
var v2 interface{}
err = toml.Unmarshal(encoded, &v2)
assert.NoError(t, err)
})
}
}
+1 -1
View File
@@ -89,7 +89,7 @@ func (n *Node) Next() *Node {
}
// Child returns a pointer to the first child node of this node. Other children
// can be accessed calling Next on the first child. Returns an nil if this Node
// can be accessed calling Next on the first child. Returns nil if this Node
// has no child.
func (n *Node) Child() *Node {
if n.child == 0 {
+1 -1
View File
@@ -1076,7 +1076,7 @@ byteLoop:
}
case c == 'T' || c == 't' || c == ':' || c == '.':
hasTime = true
case c == '+' || c == '-' || c == 'Z' || c == 'z':
case c == '+' || c == 'Z' || c == 'z':
hasTz = true
case c == ' ':
if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) {