feat: add OffsetDateTime, nesting limits and uniform Marshaler dispatch
Test / test (push) Successful in 2m18s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-19 19:36:24 +02:00
parent b695b69768
commit a6e3e3fe31
12 changed files with 631 additions and 117 deletions
+59 -11
View File
@@ -104,15 +104,33 @@ func Parse(data []byte) (map[string]any, error) {
// ctx. The context is checked between top-level statements so cancellation is
// honoured before the parser has done substantial work.
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 {
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) {
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
// 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()
}
@@ -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
// strictness.
// strictness and configurable limits on the parse it performs.
type Decoder struct {
disallowUnknown bool
maxDepth int
maxInputSize int
}
// NewDecoder returns a Decoder.
@@ -158,6 +178,28 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
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,
// 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.
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 {
return err
}
@@ -193,12 +238,15 @@ type Marshaler interface {
// 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
// argument is whatever the parser produced for that key: one of string,
// bool, int64, float64, time.Time, LocalDateTime, LocalDate, LocalTime,
// []any, or map[string]any. UnmarshalTOML may parse, inspect, or transform
// the value however it likes, 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).
// bool, int64, float64, OffsetDateTime, LocalDateTime, LocalDate, LocalTime,
// []any, or map[string]any. A tree built by hand may carry a plain time.Time
// where the parser would put an OffsetDateTime.
//
// UnmarshalTOML may parse, inspect, or transform the value however it likes,
// 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
// 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
// inline table.
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
// (offset date-time), and LocalDateTime/LocalDate/LocalTime (local
// variants). A date-time writes its seconds only when the value carries
// and OffsetDateTime (offset date-time), and LocalDateTime/LocalDate/
// LocalTime (local variants). A date-time writes its seconds only when the value carries
// them, and drops the trailing zeros of a fractional second.
// - 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