Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f19f855f1 |
@@ -1,57 +0,0 @@
|
|||||||
# 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`
|
|
||||||
|
|
||||||
### Commit Messages
|
|
||||||
|
|
||||||
- Commit messages must explain **why** the change is needed
|
|
||||||
- Keep messages clear and informative even if details are in the PR description
|
|
||||||
|
|
||||||
## 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. Title is clear and understandable for changelog
|
|
||||||
@@ -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")
|
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(
|
t := time.Date(
|
||||||
dt.Year,
|
dt.Year,
|
||||||
time.Month(dt.Month),
|
time.Month(dt.Month),
|
||||||
dt.Day,
|
dt.Day,
|
||||||
dt.Hour,
|
dt.Hour,
|
||||||
dt.Minute,
|
dt.Minute,
|
||||||
dt.Second,
|
second,
|
||||||
dt.Nanosecond,
|
dt.Nanosecond,
|
||||||
zone)
|
zone)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
"github.com/pelletier/go-toml/v2/unstable"
|
"github.com/pelletier/go-toml/v2/unstable"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ func (e *DecodeError) Key() Key {
|
|||||||
//
|
//
|
||||||
//nolint:funlen
|
//nolint:funlen
|
||||||
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
|
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
|
||||||
offset := cap(document) - cap(de.Highlight)
|
offset := danger.SubsliceOffset(document, de.Highlight)
|
||||||
|
|
||||||
errMessage := de.Error()
|
errMessage := de.Error()
|
||||||
errLine, errColumn := positionAtEnd(document[:offset])
|
errLine, errColumn := positionAtEnd(document[:offset])
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package danger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxInt = uintptr(int(^uint(0) >> 1))
|
||||||
|
|
||||||
|
func SubsliceOffset(data []byte, subslice []byte) int {
|
||||||
|
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||||
|
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
|
||||||
|
|
||||||
|
if hlp.Data < datap.Data {
|
||||||
|
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
|
||||||
|
}
|
||||||
|
offset := hlp.Data - datap.Data
|
||||||
|
|
||||||
|
if offset > maxInt {
|
||||||
|
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
intoffset := int(offset)
|
||||||
|
|
||||||
|
if intoffset > datap.Len {
|
||||||
|
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
|
||||||
|
}
|
||||||
|
|
||||||
|
if intoffset+hlp.Len > datap.Len {
|
||||||
|
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
|
||||||
|
}
|
||||||
|
|
||||||
|
return intoffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func BytesRange(start []byte, end []byte) []byte {
|
||||||
|
if start == nil || end == nil {
|
||||||
|
panic("cannot call BytesRange with nil")
|
||||||
|
}
|
||||||
|
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
|
||||||
|
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
|
||||||
|
|
||||||
|
if startp.Data > endp.Data {
|
||||||
|
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
l := startp.Len
|
||||||
|
endLen := int(endp.Data-startp.Data) + endp.Len
|
||||||
|
if endLen > l {
|
||||||
|
l = endLen
|
||||||
|
}
|
||||||
|
|
||||||
|
if l > startp.Cap {
|
||||||
|
panic(fmt.Errorf("range length is larger than capacity"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return start[:l]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
|
||||||
|
// TODO: replace with unsafe.Add when Go 1.17 is released
|
||||||
|
// https://github.com/golang/go/issues/40481
|
||||||
|
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package danger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubsliceOffsetValid(t *testing.T) {
|
||||||
|
examples := []struct {
|
||||||
|
desc string
|
||||||
|
test func() ([]byte, []byte)
|
||||||
|
offset int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
data := []byte("hello")
|
||||||
|
return data, data[1:]
|
||||||
|
},
|
||||||
|
offset: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
t.Run(e.desc, func(t *testing.T) {
|
||||||
|
d, s := e.test()
|
||||||
|
offset := danger.SubsliceOffset(d, s)
|
||||||
|
assert.Equal(t, e.offset, offset)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubsliceOffsetInvalid(t *testing.T) {
|
||||||
|
examples := []struct {
|
||||||
|
desc string
|
||||||
|
test func() ([]byte, []byte)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "unrelated arrays",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
return []byte("one"), []byte("two")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice starts before data",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[5:], full[1:]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice starts after data",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[:3], full[5:]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice ends after data",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[:5], full[3:8]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
t.Run(e.desc, func(t *testing.T) {
|
||||||
|
d, s := e.test()
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
danger.SubsliceOffset(d, s)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStride(t *testing.T) {
|
||||||
|
a := []byte{1, 2, 3, 4}
|
||||||
|
x := &a[1]
|
||||||
|
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
|
||||||
|
assert.Equal(t, &a[2], n)
|
||||||
|
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
|
||||||
|
assert.Equal(t, &a[0], n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesRange(t *testing.T) {
|
||||||
|
type fn = func() ([]byte, []byte)
|
||||||
|
examples := []struct {
|
||||||
|
desc string
|
||||||
|
test fn
|
||||||
|
expected []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[1:3], full[6:8]
|
||||||
|
},
|
||||||
|
expected: []byte("ello wo"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "full",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[0:1], full[len(full)-1:]
|
||||||
|
},
|
||||||
|
expected: []byte("hello world"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "end before start",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[len(full)-1:], full[0:1]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nils",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nils start",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
return nil, []byte("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nils end",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
return []byte("foo"), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "start is end",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[1:3], full[1:3]
|
||||||
|
},
|
||||||
|
expected: []byte("el"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "end contained in start",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
full := []byte("hello world")
|
||||||
|
return full[1:7], full[2:4]
|
||||||
|
},
|
||||||
|
expected: []byte("ello w"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "different backing arrays",
|
||||||
|
test: func() ([]byte, []byte) {
|
||||||
|
one := []byte("hello world")
|
||||||
|
two := []byte("hello world")
|
||||||
|
return one, two
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
t.Run(e.desc, func(t *testing.T) {
|
||||||
|
start, end := e.test()
|
||||||
|
if e.expected == nil {
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
danger.BytesRange(start, end)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res := danger.BytesRange(start, end)
|
||||||
|
assert.Equal(t, e.expected, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package danger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// typeID is used as key in encoder and decoder caches to enable using
|
||||||
|
// the optimize runtime.mapaccess2_fast64 function instead of the more
|
||||||
|
// expensive lookup if we were to use reflect.Type as map key.
|
||||||
|
//
|
||||||
|
// typeID holds the pointer to the reflect.Type value, which is unique
|
||||||
|
// in the program.
|
||||||
|
//
|
||||||
|
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
|
||||||
|
type TypeID unsafe.Pointer
|
||||||
|
|
||||||
|
func MakeTypeID(t reflect.Type) TypeID {
|
||||||
|
// reflect.Type has the fields:
|
||||||
|
// typ unsafe.Pointer
|
||||||
|
// ptr unsafe.Pointer
|
||||||
|
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package tracker
|
package tracker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||||
)
|
)
|
||||||
@@ -13,8 +13,8 @@ func TestEntrySize(t *testing.T) {
|
|||||||
// and a very good reason.
|
// and a very good reason.
|
||||||
maxExpectedEntrySize := 48
|
maxExpectedEntrySize := 48
|
||||||
assert.True(t,
|
assert.True(t,
|
||||||
int(reflect.TypeOf(entry{}).Size()) <= maxExpectedEntrySize,
|
int(unsafe.Sizeof(entry{})) <= maxExpectedEntrySize,
|
||||||
"Expected entry to be less than or equal to %d, got: %d",
|
"Expected entry to be less than or equal to %d, got: %d",
|
||||||
maxExpectedEntrySize, int(reflect.TypeOf(entry{}).Size()),
|
maxExpectedEntrySize, int(unsafe.Sizeof(entry{})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -94,7 +94,13 @@ type LocalDateTime struct {
|
|||||||
|
|
||||||
// AsTime converts d into a specific time instance in zone.
|
// AsTime converts d into a specific time instance in zone.
|
||||||
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
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.
|
// String returns RFC 3339 representation of d.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
"github.com/pelletier/go-toml/v2/internal/tracker"
|
"github.com/pelletier/go-toml/v2/internal/tracker"
|
||||||
"github.com/pelletier/go-toml/v2/unstable"
|
"github.com/pelletier/go-toml/v2/unstable"
|
||||||
)
|
)
|
||||||
@@ -102,5 +103,5 @@ func keyLocation(node *unstable.Node) []byte {
|
|||||||
end = k.Node().Data
|
end = k.Node().Data
|
||||||
}
|
}
|
||||||
|
|
||||||
return start[:cap(start)-cap(end)+len(end)]
|
return danger.BytesRange(start, end)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-14
@@ -12,6 +12,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
"github.com/pelletier/go-toml/v2/internal/tracker"
|
"github.com/pelletier/go-toml/v2/internal/tracker"
|
||||||
"github.com/pelletier/go-toml/v2/unstable"
|
"github.com/pelletier/go-toml/v2/unstable"
|
||||||
)
|
)
|
||||||
@@ -1293,22 +1294,13 @@ func fieldByIndex(v reflect.Value, path []int) reflect.Value {
|
|||||||
|
|
||||||
type fieldPathsMap = map[string][]int
|
type fieldPathsMap = map[string][]int
|
||||||
|
|
||||||
var globalFieldPathsCache atomic.Value // map[uintptr]fieldPathsMap
|
var globalFieldPathsCache atomic.Value // map[danger.TypeID]fieldPathsMap
|
||||||
|
|
||||||
func structFieldPath(v reflect.Value, name string) ([]int, bool) {
|
func structFieldPath(v reflect.Value, name string) ([]int, bool) {
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
// reflect.Type is an interface. We want to use the address of the underlying
|
|
||||||
// rtype as the key.
|
|
||||||
// This avoids using the interface as map key, which is slower.
|
|
||||||
//
|
|
||||||
// In the future this should be replaced by t.Pointer() if it becomes available.
|
|
||||||
//
|
|
||||||
// v.Type() returns a reflect.Type interface.
|
|
||||||
// reflect.ValueOf(t).Pointer() returns the address of the rtype.
|
|
||||||
tid := reflect.ValueOf(t).Pointer()
|
|
||||||
|
|
||||||
cache, _ := globalFieldPathsCache.Load().(map[uintptr]fieldPathsMap)
|
cache, _ := globalFieldPathsCache.Load().(map[danger.TypeID]fieldPathsMap)
|
||||||
fieldPaths, ok := cache[tid]
|
fieldPaths, ok := cache[danger.MakeTypeID(t)]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
fieldPaths = map[string][]int{}
|
fieldPaths = map[string][]int{}
|
||||||
@@ -1319,8 +1311,8 @@ func structFieldPath(v reflect.Value, name string) ([]int, bool) {
|
|||||||
fieldPaths[strings.ToLower(name)] = path
|
fieldPaths[strings.ToLower(name)] = path
|
||||||
})
|
})
|
||||||
|
|
||||||
newCache := make(map[uintptr]fieldPathsMap, len(cache)+1)
|
newCache := make(map[danger.TypeID]fieldPathsMap, len(cache)+1)
|
||||||
newCache[tid] = fieldPaths
|
newCache[danger.MakeTypeID(t)] = fieldPaths
|
||||||
for k, v := range cache {
|
for k, v := range cache {
|
||||||
newCache[k] = v
|
newCache[k] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4242,3 +4242,66 @@ func TestIssue995_SliceNonEmpty_UsesLastElement(t *testing.T) {
|
|||||||
t.Fatalf("unexpected values in allowlists: %v", got)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-6
@@ -2,6 +2,9 @@ package unstable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Iterator over a sequence of nodes.
|
// Iterator over a sequence of nodes.
|
||||||
@@ -34,7 +37,7 @@ func (c *Iterator) Next() bool {
|
|||||||
// IsLast returns true if the current node of the iterator is the last
|
// IsLast returns true if the current node of the iterator is the last
|
||||||
// one. Subsequent calls to Next() will return false.
|
// one. Subsequent calls to Next() will return false.
|
||||||
func (c *Iterator) IsLast() bool {
|
func (c *Iterator) IsLast() bool {
|
||||||
return c.node.next == nil
|
return c.node.next == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node returns a pointer to the node pointed at by the iterator.
|
// Node returns a pointer to the node pointed at by the iterator.
|
||||||
@@ -62,9 +65,11 @@ type Node struct {
|
|||||||
Raw Range // Raw bytes from the input.
|
Raw Range // Raw bytes from the input.
|
||||||
Data []byte // Node value (either allocated or referencing the input).
|
Data []byte // Node value (either allocated or referencing the input).
|
||||||
|
|
||||||
// References to other nodes.
|
// References to other nodes, as offsets in the backing array
|
||||||
next *Node // nil if last element
|
// from this node. References can go backward, so those can be
|
||||||
child *Node // nil if no child
|
// negative.
|
||||||
|
next int // 0 if last element
|
||||||
|
child int // 0 if no child
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range of bytes in the document.
|
// Range of bytes in the document.
|
||||||
@@ -75,14 +80,24 @@ type Range struct {
|
|||||||
|
|
||||||
// Next returns a pointer to the next node, or nil if there is no next node.
|
// Next returns a pointer to the next node, or nil if there is no next node.
|
||||||
func (n *Node) Next() *Node {
|
func (n *Node) Next() *Node {
|
||||||
return n.next
|
if n.next == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ptr := unsafe.Pointer(n)
|
||||||
|
size := unsafe.Sizeof(Node{})
|
||||||
|
return (*Node)(danger.Stride(ptr, size, n.next))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child returns a pointer to the first child node of this node. Other children
|
// Child returns a pointer to the first child node of this node. Other children
|
||||||
// can be accessed calling Next on the first child. Returns nil if this Node
|
// can be accessed calling Next on the first child. Returns nil if this Node
|
||||||
// has no child.
|
// has no child.
|
||||||
func (n *Node) Child() *Node {
|
func (n *Node) Child() *Node {
|
||||||
return n.child
|
if n.child == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ptr := unsafe.Pointer(n)
|
||||||
|
size := unsafe.Sizeof(Node{})
|
||||||
|
return (*Node)(danger.Stride(ptr, size, n.child))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid returns true if the node's kind is set (not to Invalid).
|
// Valid returns true if the node's kind is set (not to Invalid).
|
||||||
|
|||||||
+29
-79
@@ -4,118 +4,68 @@ package unstable
|
|||||||
//
|
//
|
||||||
// It is immutable once constructed with Builder.
|
// It is immutable once constructed with Builder.
|
||||||
type root struct {
|
type root struct {
|
||||||
first *Node
|
nodes []Node
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterator over the top level nodes.
|
// Iterator over the top level nodes.
|
||||||
func (r *root) Iterator() Iterator {
|
func (r *root) Iterator() Iterator {
|
||||||
return Iterator{node: r.first}
|
it := Iterator{}
|
||||||
|
if len(r.nodes) > 0 {
|
||||||
|
it.node = &r.nodes[0]
|
||||||
|
}
|
||||||
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
type reference struct {
|
func (r *root) at(idx reference) *Node {
|
||||||
*Node
|
return &r.nodes[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
var invalidReference = reference{}
|
type reference int
|
||||||
|
|
||||||
|
const invalidReference reference = -1
|
||||||
|
|
||||||
func (r reference) Valid() bool {
|
func (r reference) Valid() bool {
|
||||||
return r.Node != nil
|
return r != invalidReference
|
||||||
}
|
}
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
// chunks of nodes. Pointers to nodes are stable because we only append
|
tree root
|
||||||
// to the last chunk, and chunks are allocated with fixed capacity.
|
lastIdx int
|
||||||
chunks [][]Node
|
|
||||||
// current chunk index
|
|
||||||
chunkIdx int
|
|
||||||
|
|
||||||
// root node of the tree
|
|
||||||
root root
|
|
||||||
|
|
||||||
// last pushed node (for chaining)
|
|
||||||
last *Node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialChunkSize = 16
|
|
||||||
const maxChunkSize = 2048
|
|
||||||
|
|
||||||
func (b *builder) Tree() *root {
|
func (b *builder) Tree() *root {
|
||||||
return &b.root
|
return &b.tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) NodeAt(ref reference) *Node {
|
func (b *builder) NodeAt(ref reference) *Node {
|
||||||
return ref.Node
|
return b.tree.at(ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) Reset() {
|
func (b *builder) Reset() {
|
||||||
b.chunkIdx = 0
|
b.tree.nodes = b.tree.nodes[:0]
|
||||||
for i := range b.chunks {
|
b.lastIdx = 0
|
||||||
b.chunks[i] = b.chunks[i][:0]
|
|
||||||
}
|
|
||||||
b.root.first = nil
|
|
||||||
b.last = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) ensureCapacity() {
|
|
||||||
if b.chunkIdx >= len(b.chunks) {
|
|
||||||
size := initialChunkSize
|
|
||||||
if len(b.chunks) > 0 {
|
|
||||||
lastCap := cap(b.chunks[len(b.chunks)-1])
|
|
||||||
size = lastCap * 2
|
|
||||||
if size > maxChunkSize {
|
|
||||||
size = maxChunkSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.chunks = append(b.chunks, make([]Node, 0, size))
|
|
||||||
}
|
|
||||||
if len(b.chunks[b.chunkIdx]) == cap(b.chunks[b.chunkIdx]) {
|
|
||||||
b.chunkIdx++
|
|
||||||
if b.chunkIdx >= len(b.chunks) {
|
|
||||||
size := initialChunkSize
|
|
||||||
if len(b.chunks) > 0 {
|
|
||||||
lastCap := cap(b.chunks[len(b.chunks)-1])
|
|
||||||
size = lastCap * 2
|
|
||||||
if size > maxChunkSize {
|
|
||||||
size = maxChunkSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.chunks = append(b.chunks, make([]Node, 0, size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) push(n Node) *Node {
|
|
||||||
b.ensureCapacity()
|
|
||||||
chunk := &b.chunks[b.chunkIdx]
|
|
||||||
*chunk = append(*chunk, n)
|
|
||||||
return &(*chunk)[len(*chunk)-1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) Push(n Node) reference {
|
func (b *builder) Push(n Node) reference {
|
||||||
ptr := b.push(n)
|
b.lastIdx = len(b.tree.nodes)
|
||||||
if b.root.first == nil {
|
b.tree.nodes = append(b.tree.nodes, n)
|
||||||
b.root.first = ptr
|
return reference(b.lastIdx)
|
||||||
}
|
|
||||||
b.last = ptr
|
|
||||||
return reference{ptr}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) PushAndChain(n Node) reference {
|
func (b *builder) PushAndChain(n Node) reference {
|
||||||
ptr := b.push(n)
|
newIdx := len(b.tree.nodes)
|
||||||
if b.root.first == nil {
|
b.tree.nodes = append(b.tree.nodes, n)
|
||||||
b.root.first = ptr
|
if b.lastIdx >= 0 {
|
||||||
|
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
|
||||||
}
|
}
|
||||||
if b.last != nil {
|
b.lastIdx = newIdx
|
||||||
b.last.next = ptr
|
return reference(b.lastIdx)
|
||||||
}
|
|
||||||
b.last = ptr
|
|
||||||
return reference{ptr}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) AttachChild(parent reference, child reference) {
|
func (b *builder) AttachChild(parent reference, child reference) {
|
||||||
parent.child = child.Node
|
b.tree.nodes[parent].child = int(child) - int(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) Chain(from reference, to reference) {
|
func (b *builder) Chain(from reference, to reference) {
|
||||||
from.next = to.Node
|
b.tree.nodes[from].next = int(to) - int(from)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -6,6 +6,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/characters"
|
"github.com/pelletier/go-toml/v2/internal/characters"
|
||||||
|
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParserError describes an error relative to the content of the document.
|
// ParserError describes an error relative to the content of the document.
|
||||||
@@ -69,7 +70,7 @@ func (p *Parser) Data() []byte {
|
|||||||
// panics.
|
// panics.
|
||||||
func (p *Parser) Range(b []byte) Range {
|
func (p *Parser) Range(b []byte) Range {
|
||||||
return Range{
|
return Range{
|
||||||
Offset: uint32(cap(p.data) - cap(b)),
|
Offset: uint32(danger.SubsliceOffset(p.data, b)),
|
||||||
Length: uint32(len(b)),
|
Length: uint32(len(b)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,7 @@ type Shape struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) position(b []byte) Position {
|
func (p *Parser) position(b []byte) Position {
|
||||||
offset := cap(p.data) - cap(b)
|
offset := danger.SubsliceOffset(p.data, b)
|
||||||
|
|
||||||
lead := p.data[:offset]
|
lead := p.data[:offset]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user