feat: add OffsetDateTime, nesting limits and uniform Marshaler dispatch
Test / test (push) Successful in 2m18s
Test / test (push) Successful in 2m18s
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -28,6 +28,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
at most `threshold` bytes is written as an inline table instead of a header
|
at most `threshold` bytes is written as an inline table instead of a header
|
||||||
section, which shortens a document of small tables. An array of tables keeps
|
section, which shortens a document of small tables. An array of tables keeps
|
||||||
its header form, because its inline form would re-parse as a value array.
|
its header form, because its inline form would re-parse as a value array.
|
||||||
|
- `OffsetDateTime`, the Go type of the offset date-time kind, so that all four
|
||||||
|
TOML date-time kinds have one of their own. `Parse` and `Unmarshal` hand it
|
||||||
|
back where they produced a bare `time.Time` before, and `Marshal` accepts it.
|
||||||
|
Unmarshalling into a struct field of type `time.Time` keeps working, because
|
||||||
|
the plain type takes an offset date-time as it always did; code that asserts
|
||||||
|
the tree's type, and `UnmarshalTOML` implementations that expect a
|
||||||
|
`time.Time`, need the new type.
|
||||||
|
- `Decoder.MaxDepth(depth)` and `Decoder.MaxInputSize(size)` bound the parse a
|
||||||
|
`Decode` performs, and every parse carries a nesting limit in any case
|
||||||
|
(10000 levels, which no hand-written document approaches): a document that
|
||||||
|
nests arrays or inline tables deeper used to run the stack out and is now
|
||||||
|
rejected with a `SyntaxError` naming the limit.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -40,6 +52,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- An inline table that would pass the hundredth column is written across lines
|
- An inline table that would pass the hundredth column is written across lines
|
||||||
with a trailing comma and one tab of indentation per nesting level, the shape
|
with a trailing comma and one tab of indentation per nesting level, the shape
|
||||||
TOML 1.1 allows an inline table to take.
|
TOML 1.1 allows an inline table to take.
|
||||||
|
- `MarshalTOML` reaches every array element and every field, whatever the Go
|
||||||
|
kind, and its result is normalised like any other value: an element rendering
|
||||||
|
itself as a table keeps the `[[header]]` form, one rendering itself as a
|
||||||
|
scalar turns the array into a value array, and the method runs once per
|
||||||
|
element. It is found on the addressable pointer as well, so a
|
||||||
|
pointer-receiver `MarshalTOML` is called for a field or an element, exactly
|
||||||
|
as `MarshalText` is.
|
||||||
- TOML 1.1 is the acceptance contract, and TOML 1.0 is not. The compliance
|
- TOML 1.1 is the acceptance contract, and TOML 1.0 is not. The compliance
|
||||||
suite runs the 1.1 corpus alone, and the promise that every 1.0 document
|
suite runs the 1.1 corpus alone, and the promise that every 1.0 document
|
||||||
parses exactly as before is withdrawn. Nothing that parses today stops
|
parses exactly as before is withdrawn. Nothing that parses today stops
|
||||||
|
|||||||
@@ -285,8 +285,11 @@ func typeMatches(typ string, v any) bool {
|
|||||||
_, ok := v.(bool)
|
_, ok := v.(bool)
|
||||||
return ok
|
return ok
|
||||||
case "datetime":
|
case "datetime":
|
||||||
_, ok := v.(time.Time)
|
switch v.(type) {
|
||||||
return ok
|
case time.Time, interpres.OffsetDateTime:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
case "datetime-local":
|
case "datetime-local":
|
||||||
_, ok := v.(interpres.LocalDateTime)
|
_, ok := v.(interpres.LocalDateTime)
|
||||||
return ok
|
return ok
|
||||||
@@ -346,6 +349,8 @@ func tag(v any) (any, error) {
|
|||||||
return tagged("float", formatFloat(x)), nil
|
return tagged("float", formatFloat(x)), nil
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return tagged("datetime", x.Format(time.RFC3339Nano)), nil
|
return tagged("datetime", x.Format(time.RFC3339Nano)), nil
|
||||||
|
case interpres.OffsetDateTime:
|
||||||
|
return tagged("datetime", x.Format(time.RFC3339Nano)), nil
|
||||||
case interpres.LocalDateTime:
|
case interpres.LocalDateTime:
|
||||||
return tagged("datetime-local", x.Format("2006-01-02T15:04:05.999999999")), nil
|
return tagged("datetime-local", x.Format("2006-01-02T15:04:05.999999999")), nil
|
||||||
case interpres.LocalDate:
|
case interpres.LocalDate:
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ func TestRunEncoderRejectsBadInput(t *testing.T) {
|
|||||||
{"top-level-array", `[{"type": "integer", "value": "1"}]`, "must be a JSON object"},
|
{"top-level-array", `[{"type": "integer", "value": "1"}]`, "must be a JSON object"},
|
||||||
{"untagged-scalar", `{"x": 1}`, "unsupported JSON value"},
|
{"untagged-scalar", `{"x": 1}`, "unsupported JSON value"},
|
||||||
{"literal-mismatch", `{"x": {"type": "integer", "value": "1.5"}}`, "parsed as float64"},
|
{"literal-mismatch", `{"x": {"type": "integer", "value": "1.5"}}`, "parsed as float64"},
|
||||||
{"offset-for-local", `{"x": {"type": "datetime-local", "value": "1979-05-27T07:32:00Z"}}`, "parsed as time.Time"},
|
{"offset-for-local", `{"x": {"type": "datetime-local", "value": "1979-05-27T07:32:00Z"}}`, "parsed as interpres.OffsetDateTime"},
|
||||||
{"bad-literal", `{"x": {"type": "date-local", "value": "nope"}}`, "date-local"},
|
{"bad-literal", `{"x": {"type": "date-local", "value": "nope"}}`, "date-local"},
|
||||||
{"smuggled-statement", `{"x": {"type": "integer", "value": "1\nx = 2"}}`, "not a single bare value"},
|
{"smuggled-statement", `{"x": {"type": "integer", "value": "1\nx = 2"}}`, "not a single bare value"},
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -11,9 +11,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TOML distinguishes four date-time kinds. interpres decodes an offset
|
// TOML distinguishes four date-time kinds, and each has its own Go type:
|
||||||
// date-time to a plain time.Time (it carries a zone), and uses the wrapper
|
// OffsetDateTime for the offset kind, and the local wrappers below for the
|
||||||
// types below for the local variants so callers can tell them apart.
|
// three that carry no offset. A plain time.Time is accepted wherever an
|
||||||
|
// offset date-time is, on both the encoding and the decoding side, so a
|
||||||
|
// timestamp field does not have to name the wrapper.
|
||||||
|
|
||||||
|
// OffsetDateTime is a TOML offset date-time, e.g. 1979-05-27T07:32:00-07:00.
|
||||||
|
// The embedded time.Time is the instant, with the offset the document wrote.
|
||||||
|
type OffsetDateTime struct{ time.Time }
|
||||||
|
|
||||||
// LocalDateTime is a TOML local date-time with no offset, e.g.
|
// LocalDateTime is a TOML local date-time with no offset, e.g.
|
||||||
// 1979-05-27T07:32:00. The embedded time.Time is in UTC.
|
// 1979-05-27T07:32:00. The embedded time.Time is in UTC.
|
||||||
@@ -27,6 +33,12 @@ type LocalDate struct{ time.Time }
|
|||||||
// The embedded time.Time uses the zero date.
|
// The embedded time.Time uses the zero date.
|
||||||
type LocalTime struct{ time.Time }
|
type LocalTime struct{ time.Time }
|
||||||
|
|
||||||
|
// String returns the TOML-canonical rendering of the offset date-time, e.g.
|
||||||
|
// "1979-05-27T07:32Z" or "1979-05-27T07:32:00-07:00". The seconds appear only
|
||||||
|
// when the value carries them, a fractional second drops its trailing zeros,
|
||||||
|
// and an offset of zero is written "Z".
|
||||||
|
func (odt OffsetDateTime) String() string { return offsetString(odt.Time) }
|
||||||
|
|
||||||
// String returns the TOML-canonical rendering of the local date-time, e.g.
|
// String returns the TOML-canonical rendering of the local date-time, e.g.
|
||||||
// "1979-05-27T07:32" or "1979-05-27T07:32:00.5" when the time carries a
|
// "1979-05-27T07:32" or "1979-05-27T07:32:00.5" when the time carries a
|
||||||
// fractional second. TOML 1.1 makes the seconds optional, so they appear only
|
// fractional second. TOML 1.1 makes the seconds optional, so they appear only
|
||||||
@@ -129,7 +141,7 @@ func parseDateTime(tok string) (any, bool) {
|
|||||||
norm := strings.ToUpper(tok)
|
norm := strings.ToUpper(tok)
|
||||||
for _, layout := range offsetDateTimeLayouts {
|
for _, layout := range offsetDateTimeLayouts {
|
||||||
if t, err := time.Parse(layout, norm); err == nil {
|
if t, err := time.Parse(layout, norm); err == nil {
|
||||||
return t, true
|
return OffsetDateTime{t}, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, layout := range localDateTimeLayouts {
|
for _, layout := range localDateTimeLayouts {
|
||||||
|
|||||||
@@ -95,12 +95,10 @@ func (d *decoder) assign(data any, dst reflect.Value) error {
|
|||||||
return setInt(dst, v)
|
return setInt(dst, v)
|
||||||
case float64:
|
case float64:
|
||||||
return setFloat(dst, v)
|
return setFloat(dst, v)
|
||||||
|
case OffsetDateTime:
|
||||||
|
return setOffsetDateTime(v, dst)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
if dst.Type() != timeType {
|
return setDateTime(v, dst)
|
||||||
return fmt.Errorf("interpres: cannot assign datetime to %s", dst.Type())
|
|
||||||
}
|
|
||||||
dst.Set(reflect.ValueOf(v))
|
|
||||||
return nil
|
|
||||||
default:
|
default:
|
||||||
rv := reflect.ValueOf(data)
|
rv := reflect.ValueOf(data)
|
||||||
if rv.IsValid() && dst.Type() == rv.Type() {
|
if rv.IsValid() && dst.Type() == rv.Type() {
|
||||||
@@ -235,6 +233,36 @@ func (d *decoder) assignTableSlice(items []map[string]any, dst reflect.Value) er
|
|||||||
|
|
||||||
// --- low-level setters -----------------------------------------------------
|
// --- low-level setters -----------------------------------------------------
|
||||||
|
|
||||||
|
// setOffsetDateTime stores an offset date-time: in a wrapper destination as it
|
||||||
|
// is, and in a plain time.Time, which takes the instant with the offset the
|
||||||
|
// document wrote, so a timestamp field does not have to name the wrapper.
|
||||||
|
func setOffsetDateTime(v OffsetDateTime, dst reflect.Value) error {
|
||||||
|
switch dst.Type() {
|
||||||
|
case offsetDateTimeType:
|
||||||
|
dst.Set(reflect.ValueOf(v))
|
||||||
|
case timeType:
|
||||||
|
dst.Set(reflect.ValueOf(v.Time))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("interpres: cannot assign datetime to %s", dst.Type())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDateTime stores a time.Time that reached the tree directly, which is the
|
||||||
|
// shape a tree built by hand carries. Dates the parser produced arrive as
|
||||||
|
// OffsetDateTime instead.
|
||||||
|
func setDateTime(v time.Time, dst reflect.Value) error {
|
||||||
|
switch dst.Type() {
|
||||||
|
case timeType:
|
||||||
|
dst.Set(reflect.ValueOf(v))
|
||||||
|
case offsetDateTimeType:
|
||||||
|
dst.Set(reflect.ValueOf(OffsetDateTime{v}))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("interpres: cannot assign datetime to %s", dst.Type())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func setBasic(dst, val reflect.Value, kind string) error {
|
func setBasic(dst, val reflect.Value, kind string) error {
|
||||||
if dst.Kind() != val.Kind() {
|
if dst.Kind() != val.Kind() {
|
||||||
return fmt.Errorf("interpres: cannot assign %s to %s", kind, dst.Type())
|
return fmt.Errorf("interpres: cannot assign %s to %s", kind, dst.Type())
|
||||||
|
|||||||
+127
@@ -951,3 +951,130 @@ func TestQuotedStringNeverBecomesDateTime(t *testing.T) {
|
|||||||
t.Fatal("expected a quoted string to be rejected for LocalDate")
|
t.Fatal("expected a quoted string to be rejected for LocalDate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecoderMaxDepth(t *testing.T) {
|
||||||
|
deep := func(n int) []byte {
|
||||||
|
return []byte("v = " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n")
|
||||||
|
}
|
||||||
|
var cfg struct {
|
||||||
|
V any `toml:"v"`
|
||||||
|
}
|
||||||
|
if err := NewDecoder().MaxDepth(4).Decode(deep(4), &cfg); err != nil {
|
||||||
|
t.Fatalf("at the limit: %v", err)
|
||||||
|
}
|
||||||
|
err := NewDecoder().MaxDepth(4).Decode(deep(5), &cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected a nesting error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "limit of 4") {
|
||||||
|
t.Errorf("err = %v, want it to name the limit", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoderMaxInputSize(t *testing.T) {
|
||||||
|
doc := []byte("v = \"ab\"\n")
|
||||||
|
var cfg struct {
|
||||||
|
V string `toml:"v"`
|
||||||
|
}
|
||||||
|
if err := NewDecoder().MaxInputSize(len(doc)).Decode(doc, &cfg); err != nil {
|
||||||
|
t.Fatalf("at the limit: %v", err)
|
||||||
|
}
|
||||||
|
err := NewDecoder().MaxInputSize(len(doc)-1).Decode(doc, &cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected a size error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "over the limit of 8") {
|
||||||
|
t.Errorf("err = %v, want it to name the limit", err)
|
||||||
|
}
|
||||||
|
// Parse carries the nesting default and no size limit.
|
||||||
|
if _, err := Parse(doc); err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- OffsetDateTime --------------------------------------------------------
|
||||||
|
|
||||||
|
func TestOffsetDateTimeIsTheParsedType(t *testing.T) {
|
||||||
|
// A document's offset date-time arrives as the wrapper, and a plain
|
||||||
|
// time.Time destination still takes it, so a timestamp field needs no
|
||||||
|
// change to keep working.
|
||||||
|
in := []byte("stamp = 2026-06-26T10:00:00-07:00\n")
|
||||||
|
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600))
|
||||||
|
|
||||||
|
var plain struct {
|
||||||
|
Stamp time.Time `toml:"stamp"`
|
||||||
|
}
|
||||||
|
if err := Unmarshal(in, &plain); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if !plain.Stamp.Equal(want) {
|
||||||
|
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapped struct {
|
||||||
|
Stamp OffsetDateTime `toml:"stamp"`
|
||||||
|
}
|
||||||
|
if err := Unmarshal(in, &wrapped); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if !wrapped.Stamp.Time.Equal(want) {
|
||||||
|
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
|
||||||
|
}
|
||||||
|
if got := wrapped.Stamp.String(); got != "2026-06-26T10:00-07:00" {
|
||||||
|
t.Errorf("String() = %q, want 2026-06-26T10:00-07:00", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOffsetDateTimeFromHandBuiltTree(t *testing.T) {
|
||||||
|
// A tree built by hand may carry a plain time.Time, which is the other
|
||||||
|
// source of the offset kind; both date-time destinations take it.
|
||||||
|
tree := map[string]any{"stamp": time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)}
|
||||||
|
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
var plain struct {
|
||||||
|
Stamp time.Time `toml:"stamp"`
|
||||||
|
}
|
||||||
|
if err := newDecoder().decode(tree, &plain); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if !plain.Stamp.Equal(want) {
|
||||||
|
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapped struct {
|
||||||
|
Stamp OffsetDateTime `toml:"stamp"`
|
||||||
|
}
|
||||||
|
if err := newDecoder().decode(tree, &wrapped); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if !wrapped.Stamp.Time.Equal(want) {
|
||||||
|
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dateKindReceiver records the Go type UnmarshalTOML was handed.
|
||||||
|
type dateKindReceiver struct{ Kind string }
|
||||||
|
|
||||||
|
func (r *dateKindReceiver) UnmarshalTOML(data any) error {
|
||||||
|
r.Kind = fmt.Sprintf("%T", data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalerReceivesOffsetDateTime(t *testing.T) {
|
||||||
|
// The interface sees the wrapper, which names the date-time kind on its
|
||||||
|
// own; the local kinds keep their own wrappers.
|
||||||
|
var cfg struct {
|
||||||
|
O dateKindReceiver `toml:"o"`
|
||||||
|
L dateKindReceiver `toml:"l"`
|
||||||
|
}
|
||||||
|
in := []byte("o = 2026-06-26T10:00:00Z\nl = 2026-06-26T10:00:00\n")
|
||||||
|
if err := Unmarshal(in, &cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.O.Kind != "interpres.OffsetDateTime" {
|
||||||
|
t.Errorf("offset kind = %q, want interpres.OffsetDateTime", cfg.O.Kind)
|
||||||
|
}
|
||||||
|
if cfg.L.Kind != "interpres.LocalDateTime" {
|
||||||
|
t.Errorf("local kind = %q, want interpres.LocalDateTime", cfg.L.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+51
-23
@@ -74,7 +74,7 @@ and every 64 fields during the reflection walk.
|
|||||||
| integer | `int64` |
|
| integer | `int64` |
|
||||||
| float | `float64` |
|
| float | `float64` |
|
||||||
| boolean | `bool` |
|
| boolean | `bool` |
|
||||||
| offset date-time | `time.Time` |
|
| offset date-time | `OffsetDateTime` |
|
||||||
| local date-time | `LocalDateTime` |
|
| local date-time | `LocalDateTime` |
|
||||||
| local date | `LocalDate` |
|
| local date | `LocalDate` |
|
||||||
| local time | `LocalTime` |
|
| local time | `LocalTime` |
|
||||||
@@ -133,22 +133,24 @@ The decoder converts to the destination type with explicit overflow checks:
|
|||||||
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | the value must be non-negative and must not overflow the destination's own width, `uint` on a 32-bit platform included; `uint64` accepts any non-negative `int64` |
|
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | the value must be non-negative and must not overflow the destination's own width, `uint` on a 32-bit platform included; `uint64` accepts any non-negative `int64` |
|
||||||
| `float32`, `float64` | copied verbatim, except that a finite value beyond the `float32` range is an overflow error rather than a silent infinity; an integer also coerces, so TOML `5` decodes into `5.0` |
|
| `float32`, `float64` | copied verbatim, except that a finite value beyond the `float32` range is an overflow error rather than a silent infinity; an integer also coerces, so TOML `5` decodes into `5.0` |
|
||||||
| `bool`, `string` | exact kind match only, no coercion across kinds |
|
| `bool`, `string` | exact kind match only, no coercion across kinds |
|
||||||
| `time.Time` | offset date-times only; no implicit conversion to or from the local variants |
|
| `time.Time`, `OffsetDateTime` | offset date-times only; no implicit conversion to or from the local variants |
|
||||||
|
|
||||||
A conversion that the rules do not allow produces an error wrapped with the
|
A conversion that the rules do not allow produces an error wrapped with the
|
||||||
offending key or index, for example `p: interpres: integer 300 overflows uint8`.
|
offending key or index, for example `p: interpres: integer 300 overflows uint8`.
|
||||||
|
|
||||||
### Date-time values
|
### Date-time values
|
||||||
|
|
||||||
Offset date-times decode into `time.Time` and keep their offset. The local
|
Offset date-times decode into `OffsetDateTime`, whose embedded `time.Time` is the
|
||||||
variants decode into `LocalDateTime`, `LocalDate` and `LocalTime`, whose
|
instant with the offset the document wrote; a destination of the plain
|
||||||
embedded `time.Time` is normalised to UTC (midnight UTC for a local date, the
|
`time.Time` takes the same value, so a timestamp field does not have to name the
|
||||||
zero date for a local time). Every kind may omit the seconds as of TOML 1.1
|
wrapper. The local variants decode into `LocalDateTime`, `LocalDate` and
|
||||||
(`07:32`, `1979-05-27T07:32`); such a value carries a zero second, and the
|
`LocalTime`, whose embedded `time.Time` is normalised to UTC (midnight UTC for a
|
||||||
encoder writes the seconds only when the value carries them, so a document
|
local date, the zero date for a local time). Every kind may omit the seconds as
|
||||||
written without seconds comes back without them. There is no implicit
|
of TOML 1.1 (`07:32`, `1979-05-27T07:32`); such a value carries a zero second,
|
||||||
|
and the encoder writes the seconds only when the value carries them, so a
|
||||||
|
document written without seconds comes back without them. There is no implicit
|
||||||
conversion between the offset and local kinds; assigning one to the other is an
|
conversion between the offset and local kinds; assigning one to the other is an
|
||||||
error. The four types take a bare timestamp and never a quoted string, so a
|
error. The date-time types take a bare timestamp and never a quoted string, so a
|
||||||
document that writes a date-time with quotes does not decode into them, and
|
document that writes a date-time with quotes does not decode into them, and
|
||||||
neither `encoding.TextUnmarshaler` nor the embedded `time.Time` changes that.
|
neither `encoding.TextUnmarshaler` nor the embedded `time.Time` changes that.
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ type Unmarshaler interface {
|
|||||||
```
|
```
|
||||||
|
|
||||||
`data` is whatever the parser produced for that key: `string`, `bool`, `int64`,
|
`data` is whatever the parser produced for that key: `string`, `bool`, `int64`,
|
||||||
`float64`, `time.Time`, `LocalDateTime`, `LocalDate`, `LocalTime`, `[]any`, or
|
`float64`, `OffsetDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`, `[]any`, or
|
||||||
`map[string]any`. The method inspects the value and mutates its own receiver;
|
`map[string]any`. The method inspects the value and mutates its own receiver;
|
||||||
the decoder keeps whatever state the receiver stored.
|
the decoder keeps whatever state the receiver stored.
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ prefix `unmarshal text:`, for example `addr: unmarshal text: not an address`.
|
|||||||
[`UnmarshalTOML`](#custom-decoding-unmarshaler) wins over `UnmarshalText` when
|
[`UnmarshalTOML`](#custom-decoding-unmarshaler) wins over `UnmarshalText` when
|
||||||
a type implements both, and the four [date-time
|
a type implements both, and the four [date-time
|
||||||
types](#date-time-values) are excluded: a quoted string stays a string and
|
types](#date-time-values) are excluded: a quoted string stays a string and
|
||||||
never becomes a `time.Time` or one of the local wrappers.
|
never becomes an `OffsetDateTime` or one of the local wrappers.
|
||||||
|
|
||||||
### Durations
|
### Durations
|
||||||
|
|
||||||
@@ -316,7 +318,7 @@ them.
|
|||||||
By default every table is emitted with its entries grouped by kind:
|
By default every table is emitted with its entries grouped by kind:
|
||||||
|
|
||||||
1. scalars (`string`, `int64`, `float64`, `bool`, `time.Time`,
|
1. scalars (`string`, `int64`, `float64`, `bool`, `time.Time`,
|
||||||
`LocalDateTime`, `LocalDate`, `LocalTime`)
|
`OffsetDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`)
|
||||||
2. sub-tables (structs and `map[string]V` values)
|
2. sub-tables (structs and `map[string]V` values)
|
||||||
3. arrays of tables (`[]struct` and `[]map[string]V`)
|
3. arrays of tables (`[]struct` and `[]map[string]V`)
|
||||||
|
|
||||||
@@ -351,11 +353,20 @@ type Marshaler interface {
|
|||||||
The returned value is encoded as if it had been passed in place of the
|
The returned value is encoded as if it had been passed in place of the
|
||||||
receiver, so it may be a scalar, a slice, an array of tables, or another
|
receiver, so it may be a scalar, a slice, an array of tables, or another
|
||||||
struct or map, including the `Marshaler` result of another type; the encoder
|
struct or map, including the `Marshaler` result of another type; the encoder
|
||||||
recurses. An error returned from `MarshalTOML` fails the marshal wrapped with
|
recurses, and the result is normalised like any other value, so a method may
|
||||||
the key path, for example `interpres: server.port: bad timestamp`. A result
|
return a plain `int` or a `time.Duration`.
|
||||||
of `nil` with a nil error fails the same way with
|
|
||||||
`MarshalTOML returned a nil value`: nil has no TOML representation, so
|
An error returned from `MarshalTOML` fails the marshal wrapped with the key
|
||||||
dropping the field silently is not an option.
|
path, for example `interpres: server.port: bad timestamp`. A result of `nil` with
|
||||||
|
a nil error fails the same way with `MarshalTOML returned a nil value`: nil has
|
||||||
|
no TOML representation, so dropping the field silently is not an option.
|
||||||
|
|
||||||
|
The method is reached for every value the walk meets, array elements included:
|
||||||
|
an element that renders itself as a table keeps the `[[header]]` form, one that
|
||||||
|
renders itself as a scalar turns the array into a value array, and the method
|
||||||
|
runs once per element. It is looked up on the value and on its address, so a
|
||||||
|
pointer-receiver method is called for a field or an element, exactly as
|
||||||
|
`MarshalText` is.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Port int
|
type Port int
|
||||||
@@ -512,9 +523,11 @@ sequenceDiagram
|
|||||||
|
|
||||||
### `type SyntaxError struct{ Line int; Msg string }`
|
### `type SyntaxError struct{ Line int; Msg string }`
|
||||||
|
|
||||||
Describes a malformed TOML document; `Line` is 1-based and `Error()` renders as
|
Describes a document the parser rejected, with the 1-based `Line` at which it
|
||||||
`interpres: line N: msg`. Read the structured fields with a type assertion or
|
gave up and `Error()` rendering as `interpres: line N: msg`. A malformed
|
||||||
`errors.AsType`:
|
document is the usual cause; the nesting limit and an input that is not valid
|
||||||
|
UTF-8 report through the same type. Read the structured fields with a type
|
||||||
|
assertion or `errors.AsType`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if se, ok := errors.AsType[*interpres.SyntaxError](err); ok {
|
if se, ok := errors.AsType[*interpres.SyntaxError](err); ok {
|
||||||
@@ -550,6 +563,19 @@ with `DisallowUnknownFields`, then call `Decode` or `DecodeContext` any number
|
|||||||
of times. A configured `Decoder` holds no per-call state and is safe for
|
of times. A configured `Decoder` holds no per-call state and is safe for
|
||||||
concurrent use.
|
concurrent use.
|
||||||
|
|
||||||
|
| Method | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `DisallowUnknownFields()` | off | a key with no matching struct field is an error |
|
||||||
|
| `MaxDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest |
|
||||||
|
| `MaxInputSize(size int)` | no limit | bound the size of the document, in bytes |
|
||||||
|
|
||||||
|
The nesting limit protects the stack, because the parser is a recursive
|
||||||
|
descent: a deeper document is rejected with a `SyntaxError` naming the limit
|
||||||
|
rather than running the stack out. `Parse` and `ParseContext` carry that same
|
||||||
|
default but take no options. The size limit is off by default, because the
|
||||||
|
caller already holds the bytes and the size is therefore a policy, not a
|
||||||
|
protection the library can impose on its own.
|
||||||
|
|
||||||
### `type Encoder`
|
### `type Encoder`
|
||||||
|
|
||||||
Configurable emission policy, constructed with `NewEncoder`. The option state
|
Configurable emission policy, constructed with `NewEncoder`. The option state
|
||||||
@@ -587,6 +613,7 @@ See [Custom decoding](#custom-decoding-unmarshaler).
|
|||||||
### Date-time wrappers
|
### Date-time wrappers
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
type OffsetDateTime struct{ time.Time } // 1979-05-27T07:32:00Z
|
||||||
type LocalDateTime struct{ time.Time } // 1979-05-27T07:32:00
|
type LocalDateTime struct{ time.Time } // 1979-05-27T07:32:00
|
||||||
type LocalDate struct{ time.Time } // 1979-05-27
|
type LocalDate struct{ time.Time } // 1979-05-27
|
||||||
type LocalTime struct{ time.Time } // 07:32:00.999999
|
type LocalTime struct{ time.Time } // 07:32:00.999999
|
||||||
@@ -602,11 +629,12 @@ writes them through `String()`.
|
|||||||
|
|
||||||
The entry points return:
|
The entry points return:
|
||||||
|
|
||||||
- `*SyntaxError` for a malformed document, with the 1-based line
|
- `*SyntaxError` for a malformed document, with the 1-based line; the nesting
|
||||||
|
limit reports through it as well
|
||||||
- `*DecodeError` for a decoding failure, with the key path in `Path`
|
- `*DecodeError` for a decoding failure, with the key path in `Path`
|
||||||
- `*EncodeError` for an encoding failure, with the key path in `Path`
|
- `*EncodeError` for an encoding failure, with the key path in `Path`
|
||||||
- a plain error for the rest: a non-pointer decode target, a cancelled
|
- a plain error for the rest: a non-pointer decode target, a cancelled
|
||||||
context, a key that is not valid UTF-8
|
context, a key that is not valid UTF-8, an input over the size limit
|
||||||
|
|
||||||
Decode and encode failures carry the key path or element index in the typed
|
Decode and encode failures carry the key path or element index in the typed
|
||||||
wrappers above, so `errors.Is` and `errors.AsType` see through them and the
|
wrappers above, so `errors.Is` and `errors.AsType` see through them and the
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ var (
|
|||||||
localDateTimeType = reflect.TypeFor[LocalDateTime]()
|
localDateTimeType = reflect.TypeFor[LocalDateTime]()
|
||||||
localDateType = reflect.TypeFor[LocalDate]()
|
localDateType = reflect.TypeFor[LocalDate]()
|
||||||
localTimeType = reflect.TypeFor[LocalTime]()
|
localTimeType = reflect.TypeFor[LocalTime]()
|
||||||
|
offsetDateTimeType = reflect.TypeFor[OffsetDateTime]()
|
||||||
timeGoType = reflect.TypeFor[time.Time]()
|
timeGoType = reflect.TypeFor[time.Time]()
|
||||||
durationType = reflect.TypeFor[time.Duration]()
|
durationType = reflect.TypeFor[time.Duration]()
|
||||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||||
@@ -342,8 +343,7 @@ func buildMapDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
|||||||
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
|
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
|
||||||
|
|
||||||
func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
|
func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
|
||||||
if v.CanInterface() {
|
if m, ok := marshalerOf(v); ok {
|
||||||
if m, ok := v.Interface().(Marshaler); ok {
|
|
||||||
mv, err := m.MarshalTOML()
|
mv, err := m.MarshalTOML()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &EncodeError{Path: joinKey(ctx, name), Err: err}
|
return &EncodeError{Path: joinKey(ctx, name), Err: err}
|
||||||
@@ -353,7 +353,6 @@ func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
|
|||||||
}
|
}
|
||||||
v = reflect.ValueOf(mv)
|
v = reflect.ValueOf(mv)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// A type that renders itself as text becomes a TOML string, whether it is
|
// A type that renders itself as text becomes a TOML string, whether it is
|
||||||
// a scalar kind or a struct.
|
// a scalar kind or a struct.
|
||||||
s, isText, err := textValue(v)
|
s, isText, err := textValue(v)
|
||||||
@@ -431,12 +430,29 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
|||||||
return doc.appendScalar(name, []any{}, ctx)
|
return doc.appendScalar(name, []any{}, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every element is resolved through MarshalTOML first, so an element that
|
||||||
|
// renders itself as a scalar, a table or a value array is classified by
|
||||||
|
// what it produces rather than by its Go kind, and its method runs once.
|
||||||
|
elems := make([]reflect.Value, n)
|
||||||
|
for i := range n {
|
||||||
|
if i%ctxCheckInterval == 0 {
|
||||||
|
if err := doc.checkCtx(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev, err := resolveElement(v.Index(i), fmt.Sprintf("%s[%d]", joinKey(ctx, name), i))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
elems[i] = ev
|
||||||
|
}
|
||||||
|
|
||||||
// An array keeps the [[header]] form only when every element is a table.
|
// An array keeps the [[header]] form only when every element is a table.
|
||||||
// TOML lets one array mix tables with scalars, and that mix renders as a
|
// TOML lets one array mix tables with scalars, and that mix renders as a
|
||||||
// value array with the table elements written inline.
|
// value array with the table elements written inline.
|
||||||
allTables := true
|
allTables := true
|
||||||
for i := range n {
|
for _, ev := range elems {
|
||||||
if !isTableElementValue(v.Index(i)) {
|
if !ev.IsValid() || !isTableElementValue(ev) {
|
||||||
allTables = false
|
allTables = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -450,16 +466,12 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
|||||||
}
|
}
|
||||||
if allTables {
|
if allTables {
|
||||||
subs := make([]*tomlDoc, n)
|
subs := make([]*tomlDoc, n)
|
||||||
for i := range n {
|
for i, ev := range elems {
|
||||||
if i%ctxCheckInterval == 0 {
|
if i%ctxCheckInterval == 0 {
|
||||||
if err := doc.checkCtx(); err != nil {
|
if err := doc.checkCtx(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ev := followPtr(v.Index(i))
|
|
||||||
if !ev.IsValid() {
|
|
||||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errors.New("nil element")}
|
|
||||||
}
|
|
||||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||||
switch ev.Kind() {
|
switch ev.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
@@ -485,29 +497,12 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
|||||||
// Value array. Table elements normalise to map[string]any and the emitter
|
// Value array. Table elements normalise to map[string]any and the emitter
|
||||||
// writes them as inline tables.
|
// writes them as inline tables.
|
||||||
items := make([]any, n)
|
items := make([]any, n)
|
||||||
for i := range n {
|
for i, ev := range elems {
|
||||||
if i%ctxCheckInterval == 0 {
|
if i%ctxCheckInterval == 0 {
|
||||||
if err := doc.checkCtx(); err != nil {
|
if err := doc.checkCtx(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ev := followPtr(v.Index(i))
|
|
||||||
if !ev.IsValid() {
|
|
||||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errors.New("nil element")}
|
|
||||||
}
|
|
||||||
if ev.CanInterface() {
|
|
||||||
if m, ok := ev.Interface().(Marshaler); ok {
|
|
||||||
mv, err := m.MarshalTOML()
|
|
||||||
if err != nil {
|
|
||||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: err}
|
|
||||||
}
|
|
||||||
if mv == nil {
|
|
||||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: errNilMarshalTOML}
|
|
||||||
}
|
|
||||||
ev = reflect.ValueOf(mv)
|
|
||||||
ev = followPtr(ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val, err := normaliseValue(ev)
|
val, err := normaliseValue(ev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: err}
|
return &EncodeError{Path: fmt.Sprintf("%s[%d]", joinKey(ctx, name), i), Err: err}
|
||||||
@@ -517,6 +512,50 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
|
|||||||
return doc.appendScalar(name, items, ctx)
|
return doc.appendScalar(name, items, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marshalerOf finds the Marshaler a value carries: on the value itself, or on
|
||||||
|
// its address, so a pointer-receiver MarshalTOML is found on an addressable
|
||||||
|
// struct field or slice element, exactly as textMarshalerOf finds MarshalText.
|
||||||
|
func marshalerOf(v reflect.Value) (Marshaler, bool) {
|
||||||
|
if !v.CanInterface() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if m, ok := v.Interface().(Marshaler); ok {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
if v.CanAddr() {
|
||||||
|
if m, ok := v.Addr().Interface().(Marshaler); ok {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveElement looks through pointers and runs MarshalTOML, so an array
|
||||||
|
// element is classified by what its method produces. path names the element,
|
||||||
|
// for the errors the method can raise.
|
||||||
|
func resolveElement(v reflect.Value, path string) (reflect.Value, error) {
|
||||||
|
ev := followPtr(v)
|
||||||
|
if !ev.IsValid() {
|
||||||
|
return ev, &EncodeError{Path: path, Err: errors.New("nil element")}
|
||||||
|
}
|
||||||
|
m, ok := marshalerOf(ev)
|
||||||
|
if !ok {
|
||||||
|
return ev, nil
|
||||||
|
}
|
||||||
|
mv, err := m.MarshalTOML()
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, &EncodeError{Path: path, Err: err}
|
||||||
|
}
|
||||||
|
if mv == nil {
|
||||||
|
return reflect.Value{}, &EncodeError{Path: path, Err: errNilMarshalTOML}
|
||||||
|
}
|
||||||
|
ev = followPtr(reflect.ValueOf(mv))
|
||||||
|
if !ev.IsValid() {
|
||||||
|
return ev, &EncodeError{Path: path, Err: errors.New("nil element")}
|
||||||
|
}
|
||||||
|
return ev, nil
|
||||||
|
}
|
||||||
|
|
||||||
// normaliseValue converts a reflect.Value into one of the canonical scalar or
|
// normaliseValue converts a reflect.Value into one of the canonical scalar or
|
||||||
// nested-array representations the emitter understands. Slices and arrays are
|
// nested-array representations the emitter understands. Slices and arrays are
|
||||||
// recursively normalised so that nested arrays (e.g. [][]int) work.
|
// recursively normalised so that nested arrays (e.g. [][]int) work.
|
||||||
@@ -528,8 +567,7 @@ func normaliseValue(v reflect.Value) (any, error) {
|
|||||||
if v.Kind() == reflect.Interface {
|
if v.Kind() == reflect.Interface {
|
||||||
return nil, fmt.Errorf("cannot encode nil value")
|
return nil, fmt.Errorf("cannot encode nil value")
|
||||||
}
|
}
|
||||||
if v.CanInterface() {
|
if m, ok := marshalerOf(v); ok {
|
||||||
if m, ok := v.Interface().(Marshaler); ok {
|
|
||||||
mv, err := m.MarshalTOML()
|
mv, err := m.MarshalTOML()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -537,11 +575,17 @@ func normaliseValue(v reflect.Value) (any, error) {
|
|||||||
if mv == nil {
|
if mv == nil {
|
||||||
return nil, errNilMarshalTOML
|
return nil, errNilMarshalTOML
|
||||||
}
|
}
|
||||||
|
// The result is normalised like any other value, so a method may return
|
||||||
|
// a duration, a defined type or another Marshaler. A result of the
|
||||||
|
// receiver's own type is written as it is, because recursing into it
|
||||||
|
// would never end.
|
||||||
|
if rv := reflect.ValueOf(mv); rv.Type() != v.Type() {
|
||||||
|
return normaliseValue(rv)
|
||||||
|
}
|
||||||
return mv, nil
|
return mv, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// The datetime structs are TOML scalars; the emitter renders each of them.
|
// The datetime structs are TOML scalars; the emitter renders each of them.
|
||||||
if t := v.Type(); t == timeGoType || isLocalDateType(t) {
|
if isScalarStruct(v.Type()) {
|
||||||
return v.Interface(), nil
|
return v.Interface(), nil
|
||||||
}
|
}
|
||||||
// TOML has no duration type, so a duration goes out in its canonical Go
|
// TOML has no duration type, so a duration goes out in its canonical Go
|
||||||
@@ -622,24 +666,24 @@ func followPtr(v reflect.Value) reflect.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isScalarStruct reports whether t is a struct type that the encoder treats
|
// isScalarStruct reports whether t is a struct type that the encoder treats
|
||||||
// as a TOML scalar (time.Time, LocalDateTime, LocalDate, LocalTime).
|
// as a TOML scalar: time.Time and the four date-time wrappers.
|
||||||
func isScalarStruct(t reflect.Type) bool {
|
func isScalarStruct(t reflect.Type) bool {
|
||||||
return t == timeGoType || isLocalDateType(t)
|
return t == timeGoType || t == offsetDateTimeType || isLocalDateType(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLocalDateType(t reflect.Type) bool {
|
func isLocalDateType(t reflect.Type) bool {
|
||||||
return t == localDateTimeType || t == localDateType || t == localTimeType
|
return t == localDateTimeType || t == localDateType || t == localTimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDateTimeType reports whether t is one of the four TOML date-time types,
|
// isDateTimeType reports whether t is one of the date-time types, which the
|
||||||
// which the encoder emits as bare atoms. Pointers are looked through. The types
|
// encoder emits as bare atoms. Pointers are looked through. The types carry
|
||||||
// carry time.Time's text methods through an embedded field, and the atom form
|
// time.Time's text methods through an embedded field, and the atom form takes
|
||||||
// takes precedence over them.
|
// precedence over them.
|
||||||
func isDateTimeType(t reflect.Type) bool {
|
func isDateTimeType(t reflect.Type) bool {
|
||||||
for t.Kind() == reflect.Pointer {
|
for t.Kind() == reflect.Pointer {
|
||||||
t = t.Elem()
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
return t == timeGoType || isLocalDateType(t)
|
return isScalarStruct(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTextMarshalerType reports whether t or *t implements
|
// isTextMarshalerType reports whether t or *t implements
|
||||||
@@ -945,6 +989,9 @@ func (e *encoder) writeValue(val any) error {
|
|||||||
case time.Time:
|
case time.Time:
|
||||||
e.buf.WriteString(offsetString(v))
|
e.buf.WriteString(offsetString(v))
|
||||||
return nil
|
return nil
|
||||||
|
case OffsetDateTime:
|
||||||
|
e.buf.WriteString(v.String())
|
||||||
|
return nil
|
||||||
case LocalDateTime:
|
case LocalDateTime:
|
||||||
e.buf.WriteString(v.String())
|
e.buf.WriteString(v.String())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+146
@@ -1599,6 +1599,7 @@ func TestMarshalDateTimeRendering(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"offset-zero-seconds", base, "v = 2026-06-26T10:00Z\n"},
|
{"offset-zero-seconds", base, "v = 2026-06-26T10:00Z\n"},
|
||||||
|
{"offset-wrapper", OffsetDateTime{Time: base}, "v = 2026-06-26T10:00Z\n"},
|
||||||
{"offset-seconds", base.Add(30 * time.Second), "v = 2026-06-26T10:00:30Z\n"},
|
{"offset-seconds", base.Add(30 * time.Second), "v = 2026-06-26T10:00:30Z\n"},
|
||||||
{"offset-fraction", base.Add(500 * time.Millisecond), "v = 2026-06-26T10:00:00.5Z\n"},
|
{"offset-fraction", base.Add(500 * time.Millisecond), "v = 2026-06-26T10:00:00.5Z\n"},
|
||||||
{"offset-zone", time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600)), "v = 2026-06-26T10:00-07:00\n"},
|
{"offset-zone", time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600)), "v = 2026-06-26T10:00-07:00\n"},
|
||||||
@@ -1800,3 +1801,148 @@ func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
|
|||||||
t.Errorf("items = %#v, want []map[string]any", tree["items"])
|
t.Errorf("items = %#v, want []map[string]any", tree["items"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Marshaler on array elements -------------------------------------------
|
||||||
|
|
||||||
|
// countingMarshaler reports how often its method ran, so a test can check that
|
||||||
|
// the encoder resolves an element once.
|
||||||
|
type countingMarshaler struct{ calls *int }
|
||||||
|
|
||||||
|
func (c countingMarshaler) MarshalTOML() (any, error) {
|
||||||
|
*c.calls++
|
||||||
|
return map[string]any{"n": int64(*c.calls)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ptrMarshaler carries MarshalTOML on the pointer receiver only.
|
||||||
|
type ptrMarshaler struct{ V string }
|
||||||
|
|
||||||
|
func (p *ptrMarshaler) MarshalTOML() (any, error) { return map[string]any{"v": p.V}, nil }
|
||||||
|
|
||||||
|
func TestMarshalerElementOfArrayOfTables(t *testing.T) {
|
||||||
|
// An element is classified by what MarshalTOML returns, so methods that
|
||||||
|
// render tables keep the [[header]] form the Go kind would have given them.
|
||||||
|
type Cfg struct {
|
||||||
|
Items []marshalerFunc `toml:"items"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{Items: []marshalerFunc{
|
||||||
|
func() (any, error) { return map[string]any{"k": "a"}, nil },
|
||||||
|
func() (any, error) { return map[string]any{"k": "b"}, nil },
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "[[items]]\nk = \"a\"\n\n[[items]]\nk = \"b\"\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalerElementScalarResultMakesValueArray(t *testing.T) {
|
||||||
|
// One element rendering itself as a scalar turns the whole array into a
|
||||||
|
// value array, with the table elements written inline.
|
||||||
|
type Cfg struct {
|
||||||
|
Items []marshalerFunc `toml:"items"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{Items: []marshalerFunc{
|
||||||
|
func() (any, error) { return map[string]any{"k": "a"}, nil },
|
||||||
|
func() (any, error) { return "x", nil },
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "items = [{k = \"a\"}, \"x\"]\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalerElementRunsOnce(t *testing.T) {
|
||||||
|
// Classification and emission share one result, so the method runs exactly
|
||||||
|
// once per element even when it decides the array's form.
|
||||||
|
calls := 0
|
||||||
|
type Cfg struct {
|
||||||
|
Items []countingMarshaler `toml:"items"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{Items: []countingMarshaler{{calls: &calls}, {calls: &calls}, {calls: &calls}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if calls != 3 {
|
||||||
|
t.Errorf("MarshalTOML ran %d times, want 3", calls)
|
||||||
|
}
|
||||||
|
want := "[[items]]\nn = 1\n\n[[items]]\nn = 2\n\n[[items]]\nn = 3\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalerPointerReceiverOnElement(t *testing.T) {
|
||||||
|
// A slice element is addressable, so a pointer-receiver MarshalTOML is
|
||||||
|
// found there, and the method's table keeps the [[header]] form.
|
||||||
|
type Cfg struct {
|
||||||
|
Items []ptrMarshaler `toml:"items"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(&Cfg{Items: []ptrMarshaler{{V: "a"}, {V: "b"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "[[items]]\nv = \"a\"\n\n[[items]]\nv = \"b\"\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same method is found on an addressable struct field, whose result is
|
||||||
|
// a table and so keeps a header section.
|
||||||
|
type Field struct {
|
||||||
|
F ptrMarshaler `toml:"f"`
|
||||||
|
}
|
||||||
|
out, err = Marshal(&Field{F: ptrMarshaler{V: "c"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if want := "[f]\nv = \"c\"\n"; string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalerElementErrorCarriesPath(t *testing.T) {
|
||||||
|
type Cfg struct {
|
||||||
|
Items []any `toml:"items"`
|
||||||
|
}
|
||||||
|
_, err := Marshal(Cfg{Items: []any{map[string]any{"k": "a"}, failingMarshalerFunc{}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error from MarshalTOML")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "oops") {
|
||||||
|
t.Errorf("err = %v, want substring \"oops\"", err)
|
||||||
|
}
|
||||||
|
ee, ok := errors.AsType[*EncodeError](err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
if ee.Path != "items[1]" {
|
||||||
|
t.Fatalf("Path = %q, want %q", ee.Path, "items[1]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalerResultIsNormalised(t *testing.T) {
|
||||||
|
// A result is normalised like any other value, so a method may return a
|
||||||
|
// plain int or a duration where the Go kind alone would not encode.
|
||||||
|
type Cfg struct {
|
||||||
|
Plain marshalerFunc `toml:"plain"`
|
||||||
|
Duration marshalerFunc `toml:"duration"`
|
||||||
|
Elements []any `toml:"elements"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{
|
||||||
|
Plain: func() (any, error) { return 7, nil },
|
||||||
|
Duration: func() (any, error) { return 90 * time.Minute, nil },
|
||||||
|
Elements: []any{marshalerFunc(func() (any, error) { return 8, nil })},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "plain = 7\nduration = \"1h30m0s\"\nelements = [8]\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+59
-11
@@ -104,15 +104,33 @@ func Parse(data []byte) (map[string]any, error) {
|
|||||||
// ctx. The context is checked between top-level statements so cancellation is
|
// ctx. The context is checked between top-level statements so cancellation is
|
||||||
// honoured before the parser has done substantial work.
|
// honoured before the parser has done substantial work.
|
||||||
func ParseContext(ctx context.Context, data []byte) (map[string]any, error) {
|
func ParseContext(ctx context.Context, data []byte) (map[string]any, error) {
|
||||||
|
return parseWithOptions(ctx, data, parseOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptions bound the work one parse may do. A zero field takes the
|
||||||
|
// default.
|
||||||
|
type parseOptions struct {
|
||||||
|
maxDepth int
|
||||||
|
maxInputSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWithOptions(ctx context.Context, data []byte, opts parseOptions) (map[string]any, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if opts.maxInputSize > 0 && len(data) > opts.maxInputSize {
|
||||||
|
return nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
|
||||||
|
}
|
||||||
if !utf8.Valid(data) {
|
if !utf8.Valid(data) {
|
||||||
return nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"}
|
return nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"}
|
||||||
}
|
}
|
||||||
|
maxDepth := opts.maxDepth
|
||||||
|
if maxDepth <= 0 {
|
||||||
|
maxDepth = maxNestingDepth
|
||||||
|
}
|
||||||
// The parser scans data in place; it only reads the buffer, and every
|
// The parser scans data in place; it only reads the buffer, and every
|
||||||
// string it stores in the tree is copied out of it.
|
// string it stores in the tree is copied out of it.
|
||||||
p := &parser{src: data, line: 1, ctx: ctx}
|
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth}
|
||||||
return p.parse()
|
return p.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +161,11 @@ func UnmarshalContext(ctx context.Context, data []byte, v any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A Decoder decodes a TOML document into a Go value with configurable
|
// A Decoder decodes a TOML document into a Go value with configurable
|
||||||
// strictness.
|
// strictness and configurable limits on the parse it performs.
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
disallowUnknown bool
|
disallowUnknown bool
|
||||||
|
maxDepth int
|
||||||
|
maxInputSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDecoder returns a Decoder.
|
// NewDecoder returns a Decoder.
|
||||||
@@ -158,6 +178,28 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxDepth bounds how deeply arrays and inline tables may nest in a document
|
||||||
|
// this decoder accepts. The parser is a recursive descent, so a document that
|
||||||
|
// nests without bound would exhaust the stack; one that nests deeper than the
|
||||||
|
// limit is rejected with a SyntaxError naming it instead. Use 0 or any
|
||||||
|
// negative value for the default of 10000, which no hand-written document
|
||||||
|
// approaches.
|
||||||
|
func (d *Decoder) MaxDepth(depth int) *Decoder {
|
||||||
|
d.maxDepth = depth
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxInputSize bounds the size of a document this decoder accepts, in bytes; a
|
||||||
|
// larger one is rejected before parsing starts. Use 0 or any negative value for
|
||||||
|
// no limit, which is the default: the caller already holds the bytes, so the
|
||||||
|
// size is a policy the caller sets rather than a protection the library
|
||||||
|
// imposes on its own. Parse and ParseContext take no limit beyond the nesting
|
||||||
|
// default.
|
||||||
|
func (d *Decoder) MaxInputSize(size int) *Decoder {
|
||||||
|
d.maxInputSize = size
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
// Decode parses data and stores the result in the value pointed to by v,
|
// Decode parses data and stores the result in the value pointed to by v,
|
||||||
// honouring the decoder's strictness settings.
|
// honouring the decoder's strictness settings.
|
||||||
//
|
//
|
||||||
@@ -168,7 +210,10 @@ func (d *Decoder) Decode(data []byte, v any) error {
|
|||||||
|
|
||||||
// DecodeContext is the cancellable variant of Decode.
|
// DecodeContext is the cancellable variant of Decode.
|
||||||
func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
|
func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
|
||||||
tree, err := ParseContext(ctx, data)
|
tree, err := parseWithOptions(ctx, data, parseOptions{
|
||||||
|
maxDepth: d.maxDepth,
|
||||||
|
maxInputSize: d.maxInputSize,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -193,12 +238,15 @@ type Marshaler interface {
|
|||||||
// Unmarshaler is the inverse of Marshaler: a type that wants control over
|
// Unmarshaler is the inverse of Marshaler: a type that wants control over
|
||||||
// how it is decoded from a TOML value may implement UnmarshalTOML. The data
|
// how it is decoded from a TOML value may implement UnmarshalTOML. The data
|
||||||
// argument is whatever the parser produced for that key: one of string,
|
// argument is whatever the parser produced for that key: one of string,
|
||||||
// bool, int64, float64, time.Time, LocalDateTime, LocalDate, LocalTime,
|
// bool, int64, float64, OffsetDateTime, LocalDateTime, LocalDate, LocalTime,
|
||||||
// []any, or map[string]any. UnmarshalTOML may parse, inspect, or transform
|
// []any, or map[string]any. A tree built by hand may carry a plain time.Time
|
||||||
// the value however it likes, then store the result by mutating its
|
// where the parser would put an OffsetDateTime.
|
||||||
// receiver through the standard pointer-indirection rules of the reflect
|
//
|
||||||
// package (i.e. via reflect.Value.Set or by reassigning fields through a
|
// UnmarshalTOML may parse, inspect, or transform the value however it likes,
|
||||||
// pointer the receiver holds).
|
// then store the result by mutating its receiver through the standard
|
||||||
|
// pointer-indirection rules of the reflect package (i.e. via
|
||||||
|
// reflect.Value.Set or by reassigning fields through a pointer the receiver
|
||||||
|
// holds).
|
||||||
//
|
//
|
||||||
// UnmarshalTOML is invoked from (*Decoder).Decode / Unmarshal when the
|
// UnmarshalTOML is invoked from (*Decoder).Decode / Unmarshal when the
|
||||||
// destination type implements the interface. The decoder does not need to
|
// destination type implements the interface. The decoder does not need to
|
||||||
@@ -231,8 +279,8 @@ type Unmarshaler interface {
|
|||||||
// value array (for example an inline table in a mixed array) emits as an
|
// value array (for example an inline table in a mixed array) emits as an
|
||||||
// inline table.
|
// inline table.
|
||||||
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
|
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
|
||||||
// (offset date-time), and LocalDateTime/LocalDate/LocalTime (local
|
// and OffsetDateTime (offset date-time), and LocalDateTime/LocalDate/
|
||||||
// variants). A date-time writes its seconds only when the value carries
|
// LocalTime (local variants). A date-time writes its seconds only when the value carries
|
||||||
// them, and drops the trailing zeros of a fractional second.
|
// them, and drops the trailing zeros of a fractional second.
|
||||||
// - A table element of a value array, and a sub-table inlined by
|
// - A table element of a value array, and a sub-table inlined by
|
||||||
// Encoder.InlineTables, is written as an inline table, across lines when it
|
// Encoder.InlineTables, is written as an inline table, across lines when it
|
||||||
|
|||||||
+32
-9
@@ -4,6 +4,7 @@
|
|||||||
package interpres
|
package interpres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -192,7 +193,7 @@ clock = 07:32:00
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
}
|
}
|
||||||
if off, ok := tree["offset"].(time.Time); !ok || off.Year() != 1979 || off.Hour() != 7 {
|
if off, ok := tree["offset"].(OffsetDateTime); !ok || off.Year() != 1979 || off.Hour() != 7 {
|
||||||
t.Errorf("offset = %#v (%T)", tree["offset"], tree["offset"])
|
t.Errorf("offset = %#v (%T)", tree["offset"], tree["offset"])
|
||||||
}
|
}
|
||||||
if ldt, ok := tree["local"].(LocalDateTime); !ok || ldt.Year() != 1979 || ldt.Hour() != 7 {
|
if ldt, ok := tree["local"].(LocalDateTime); !ok || ldt.Year() != 1979 || ldt.Hour() != 7 {
|
||||||
@@ -211,11 +212,11 @@ func TestDateTimeFormats(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
}
|
}
|
||||||
if _, ok := tree["a"].(time.Time); !ok {
|
if _, ok := tree["a"].(OffsetDateTime); !ok {
|
||||||
t.Errorf("a is %T, want time.Time", tree["a"])
|
t.Errorf("a is %T, want OffsetDateTime", tree["a"])
|
||||||
}
|
}
|
||||||
if _, ok := tree["b"].(time.Time); !ok {
|
if _, ok := tree["b"].(OffsetDateTime); !ok {
|
||||||
t.Errorf("b is %T, want time.Time", tree["b"])
|
t.Errorf("b is %T, want OffsetDateTime", tree["b"])
|
||||||
}
|
}
|
||||||
if _, ok := tree["c"].(LocalDateTime); !ok {
|
if _, ok := tree["c"].(LocalDateTime); !ok {
|
||||||
t.Errorf("c is %T, want LocalDateTime", tree["c"])
|
t.Errorf("c is %T, want LocalDateTime", tree["c"])
|
||||||
@@ -419,11 +420,11 @@ func TestParseAcceptsOffsetBounds(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
}
|
}
|
||||||
a := tree["a"].(time.Time)
|
a := tree["a"].(OffsetDateTime)
|
||||||
if _, offset := a.Zone(); offset != 23*3600+59*60 {
|
if _, offset := a.Zone(); offset != 23*3600+59*60 {
|
||||||
t.Fatalf("a offset = %d, want %d", offset, 23*3600+59*60)
|
t.Fatalf("a offset = %d, want %d", offset, 23*3600+59*60)
|
||||||
}
|
}
|
||||||
b := tree["b"].(time.Time)
|
b := tree["b"].(OffsetDateTime)
|
||||||
if _, offset := b.Zone(); offset != -(23*3600 + 59*60) {
|
if _, offset := b.Zone(); offset != -(23*3600 + 59*60) {
|
||||||
t.Fatalf("b offset = %d", offset)
|
t.Fatalf("b offset = %d", offset)
|
||||||
}
|
}
|
||||||
@@ -619,10 +620,10 @@ odt2 = 1979-05-27 07:32-07:00
|
|||||||
if got := tree["dt"].(LocalDateTime).String(); got != "1979-05-27T07:32" {
|
if got := tree["dt"].(LocalDateTime).String(); got != "1979-05-27T07:32" {
|
||||||
t.Errorf("dt = %q, want %q", got, "1979-05-27T07:32")
|
t.Errorf("dt = %q, want %q", got, "1979-05-27T07:32")
|
||||||
}
|
}
|
||||||
if got := tree["odt1"].(time.Time).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00Z" {
|
if got := tree["odt1"].(OffsetDateTime).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00Z" {
|
||||||
t.Errorf("odt1 = %q", got)
|
t.Errorf("odt1 = %q", got)
|
||||||
}
|
}
|
||||||
if got := tree["odt2"].(time.Time).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00-07:00" {
|
if got := tree["odt2"].(OffsetDateTime).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00-07:00" {
|
||||||
t.Errorf("odt2 = %q", got)
|
t.Errorf("odt2 = %q", got)
|
||||||
}
|
}
|
||||||
// The fraction still requires the seconds it belongs to.
|
// The fraction still requires the seconds it belongs to.
|
||||||
@@ -703,3 +704,25 @@ func TestParseAcceptsMultilineInlineTables(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseNestingLimit(t *testing.T) {
|
||||||
|
// The parser is a recursive descent, so a document that nests without bound
|
||||||
|
// is rejected instead of exhausting the stack.
|
||||||
|
deep := func(n int) []byte {
|
||||||
|
return []byte("v = " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n")
|
||||||
|
}
|
||||||
|
if _, err := Parse(deep(100)); err != nil {
|
||||||
|
t.Fatalf("a document well inside the limit: %v", err)
|
||||||
|
}
|
||||||
|
_, err := Parse(deep(maxNestingDepth + 1))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected a nesting error")
|
||||||
|
}
|
||||||
|
var se *SyntaxError
|
||||||
|
if !errors.As(err, &se) {
|
||||||
|
t.Fatalf("expected a *SyntaxError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(se.Msg, "nesting") {
|
||||||
|
t.Errorf("Msg = %q, want it to name the nesting limit", se.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ type parser struct {
|
|||||||
line int
|
line int
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
|
// maxDepth and depth bound the nesting the recursive descent may follow:
|
||||||
|
// arrays and inline tables nest through parseValue, and without a limit a
|
||||||
|
// hostile document would exhaust the stack.
|
||||||
|
maxDepth int
|
||||||
|
depth int
|
||||||
|
|
||||||
root map[string]any
|
root map[string]any
|
||||||
current map[string]any
|
current map[string]any
|
||||||
headers map[string]bool
|
headers map[string]bool
|
||||||
@@ -40,6 +46,23 @@ type parser struct {
|
|||||||
currentPath []string
|
currentPath []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxNestingDepth bounds how deeply arrays and inline tables may nest when no
|
||||||
|
// limit is set. It matches the default encoding/json uses for the same reason,
|
||||||
|
// and sits far above any document a person writes.
|
||||||
|
const maxNestingDepth = 10000
|
||||||
|
|
||||||
|
// enterNesting counts one level of array or inline-table nesting and reports a
|
||||||
|
// document that nests deeper than the limit allows.
|
||||||
|
func (p *parser) enterNesting() error {
|
||||||
|
p.depth++
|
||||||
|
if p.depth > p.maxDepth {
|
||||||
|
return p.errf("nesting exceeds the limit of %d", p.maxDepth)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) leaveNesting() { p.depth-- }
|
||||||
|
|
||||||
func (p *parser) parse() (map[string]any, error) {
|
func (p *parser) parse() (map[string]any, error) {
|
||||||
p.root = map[string]any{}
|
p.root = map[string]any{}
|
||||||
p.current = p.root
|
p.current = p.root
|
||||||
@@ -676,6 +699,10 @@ func (p *parser) readUnicode(n int) (rune, error) {
|
|||||||
// --- arrays and inline tables ---------------------------------------------
|
// --- arrays and inline tables ---------------------------------------------
|
||||||
|
|
||||||
func (p *parser) parseArray() (any, error) {
|
func (p *parser) parseArray() (any, error) {
|
||||||
|
if err := p.enterNesting(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer p.leaveNesting()
|
||||||
p.pos++ // '['
|
p.pos++ // '['
|
||||||
arr := []any{}
|
arr := []any{}
|
||||||
for {
|
for {
|
||||||
@@ -713,6 +740,10 @@ func (p *parser) parseArray() (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseInlineTable() (any, error) {
|
func (p *parser) parseInlineTable() (any, error) {
|
||||||
|
if err := p.enterNesting(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer p.leaveNesting()
|
||||||
p.pos++ // '{'
|
p.pos++ // '{'
|
||||||
tbl := map[string]any{}
|
tbl := map[string]any{}
|
||||||
assigned := map[string]bool{}
|
assigned := map[string]bool{}
|
||||||
|
|||||||
Reference in New Issue
Block a user