Reject unsupported inline pointer schemas safely

This commit is contained in:
2026-09-06 19:12:19 -05:00
parent be2ba9a20b
commit 3e70ad1285
4 changed files with 55 additions and 1 deletions

View File

@@ -589,7 +589,9 @@ Unexported fields and `yaml:"-"` are ignored. `omitempty` is accepted but does n
change binding or projection. An explicit `yaml:",inline"` flattens a struct
or pointer-to-struct, or designates at most one string-keyed inline map for otherwise
unmatched names. Ambiguous names, named inline fields, multiple inline maps, and
unsupported or malformed tag options are schema errors. Embedded fields are ordinary
unsupported or malformed tag options are schema errors. Inline pointers must
terminate in a struct; pointer-to-map and recursive pointer-only inline declarations
are rejected before the defaults factory runs. Embedded fields are ordinary
named fields unless explicitly inlined. `configkit:"secret"` is the only current
ConfigKit field tag and marks its bound field or container for secret classification
and redacted inspection.

View File

@@ -188,6 +188,12 @@ existing descendants remain explainable after parent replacement.
## 7. P2 — Reject unsupported inline pointer-to-map schemas safely
Status: complete. Schema construction rejects inline map pointers, including
multiple pointer levels, and detects nonterminating inline pointer chains. Public
regressions verify safe errors before defaults run; supported inline coverage stays
green. Full tests pass with the issue-8 TMPDIR workaround; vet, build and whitespace
checks pass.
Evidence: [schema.go](../../schema.go), `describeFields`, dereferences inline field
types before choosing the map branch, then stores a map descriptor for a field
whose actual reflected value is a pointer. [projection.go](../../projection.go)

View File

@@ -212,9 +212,17 @@ func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current refle
return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"}
}
inlineType := field.Type
seenPointers := make(map[reflect.Type]bool)
for inlineType.Kind() == reflect.Pointer {
if seenPointers[inlineType] {
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"}
}
seenPointers[inlineType] = true
inlineType = inlineType.Elem()
}
if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer {
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"}
}
switch inlineType.Kind() {
case reflect.Struct:
if b.building[inlineType] {

View File

@@ -1,6 +1,7 @@
package configkit
import (
"errors"
"reflect"
"testing"
"time"
@@ -302,3 +303,40 @@ func mustPath(t *testing.T, pointer string) Path {
}
return path
}
func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
type cfg struct {
Extra *map[string]int `yaml:",inline"`
}
for _, nonNil := range []bool{false, true} {
called := false
_, err := Load(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
called = true
if nonNil {
m := map[string]int{"value": 1}
return cfg{Extra: &m}
}
return cfg{}
}}})
var loadErr *LoadError
if !errors.As(err, &loadErr) || called {
t.Fatalf("invalid inline schema reached defaults: called=%v error=%v", called, err)
}
if len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeSchemaInline {
t.Fatal("missing inline schema diagnostic")
}
}
type pointer *pointer
type recursive struct {
Extra pointer `yaml:",inline"`
}
if _, err := Load[recursive](NoSource()); err == nil {
t.Fatal("recursive inline pointer was accepted")
}
type double struct {
Extra **map[string]int `yaml:",inline"`
}
if _, err := Load[double](NoSource()); err == nil {
t.Fatal("double inline map pointer was accepted")
}
}