fix(encode): reject a nil MarshalTOML result

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-17 23:11:10 +02:00
parent d5327568fb
commit 4def1b3e8b
4 changed files with 61 additions and 2 deletions
+4
View File
@@ -45,6 +45,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A `MarshalTOML` result of `nil` with a nil error fails the marshal with
`MarshalTOML returned a nil value`. The field silently vanished before, and
inside a value array the nil result reached reflection as a zero value and
panicked.
- Decoding into a struct that embeds a pointer to itself terminates. The
schema walk recursed through the embedded type forever, so such a
`Unmarshal` call hung the process; the walk now tracks the struct types on
+4 -1
View File
@@ -309,7 +309,10 @@ 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
struct or map, including the `Marshaler` result of another type; the encoder
recurses. An error returned from `MarshalTOML` fails the marshal wrapped with
the key path, for example `interpres: server.port: bad timestamp`.
the key 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.
```go
type Port int
+20 -1
View File
@@ -300,6 +300,12 @@ func buildMapDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
// --- reflection walk: field dispatch -------------------------------------
// errNilMarshalTOML reports a Marshaler whose method returned a nil value
// with no error. nil has no TOML representation, so dropping the field
// silently or panicking on the invalid reflect.Value would both hide the
// contract violation.
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
if v.CanInterface() {
if m, ok := v.Interface().(Marshaler); ok {
@@ -307,6 +313,9 @@ func addField(doc *tomlDoc, name string, v reflect.Value, ctx string) error {
if err != nil {
return &EncodeError{Path: joinKey(ctx, name), Err: err}
}
if mv == nil {
return &EncodeError{Path: joinKey(ctx, name), Err: errNilMarshalTOML}
}
v = reflect.ValueOf(mv)
}
}
@@ -448,6 +457,9 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error
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)
}
@@ -474,7 +486,14 @@ func normaliseValue(v reflect.Value) (any, error) {
}
if v.CanInterface() {
if m, ok := v.Interface().(Marshaler); ok {
return m.MarshalTOML()
mv, err := m.MarshalTOML()
if err != nil {
return nil, err
}
if mv == nil {
return nil, errNilMarshalTOML
}
return mv, nil
}
}
// The datetime structs are TOML scalars; the emitter renders each of them.
+33
View File
@@ -383,6 +383,39 @@ func TestMarshalerErrorPropagates(t *testing.T) {
}
}
// nilMarshalerFunc is a Marshaler whose method returns nil with no error.
type nilMarshalerFunc struct{}
func (nilMarshalerFunc) MarshalTOML() (any, error) { return nil, nil }
func TestMarshalRejectsNilMarshalerResult(t *testing.T) {
// nil has no TOML representation, so a MarshalTOML result of nil is an
// error, not a silently dropped field.
_, err := Marshal(struct {
F nilMarshalerFunc `toml:"f"`
}{})
if err == nil {
t.Fatal("expected an error for a nil MarshalTOML result")
}
ee, ok := errors.AsType[*EncodeError](err)
if !ok {
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
}
if ee.Path != "f" {
t.Fatalf("Path = %q, want %q", ee.Path, "f")
}
// Inside a value array the nil result used to reach reflection as a zero
// Value and panic.
_, err = Marshal(map[string]any{"arr": []any{1, nilMarshalerFunc{}}})
if err == nil {
t.Fatal("expected an error for a nil MarshalTOML result in an array")
}
if !strings.Contains(err.Error(), "MarshalTOML returned a nil value") {
t.Errorf("err = %v, want the nil-result message", err)
}
}
// Two fields that resolve to one TOML key must marshal as one key, resolved
// the way the decoder resolves it, or the output would carry a duplicate key
// and never re-parse.