From 3cd3f8f3175bf3f281bc8db02eefad3fe7755a31 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 17:50:42 +0200 Subject: [PATCH] Spend the struct cap on embedded structs too, word it for any struct field, and route path advice through the name check --- README.md | 7 ++++--- inline.go | 15 +++++++++++++-- node.go | 10 ++++++++++ struct.go | 33 ++++++++++++++++++++------------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 61108b1..ed89bcb 100644 --- a/README.md +++ b/README.md @@ -193,9 +193,10 @@ naming a kind that holds it. The fields an embedded struct promotes are columns same record; a named struct field, or a pointer to one, fills from its own tags as a record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep -their values, and so does a pointer back to a struct already being filled. The first call for a type compiles its tags and reports -what they get wrong, with the same error on every later call; a `datatype` in a tag -names the Go type that already sets it. +their values, and so does a pointer back to a struct already being filled; a type +whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The +first call for a type compiles its tags and reports what they get wrong, with the same +error on every later call; a `datatype` in a tag names the Go type that already sets it. A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only when drawn from one goroutine. Changing how a value is composed shifts the seeded diff --git a/inline.go b/inline.go index ffd8789..ca693dc 100644 --- a/inline.go +++ b/inline.go @@ -65,7 +65,7 @@ func IsTemplate(arg string) (bool, error) { func isTemplate(arg string) (bool, error) { if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) { if path, lone := loneReference(arg); lone { - return false, fmt.Errorf("%s is the path %s written as a template; write %s", arg, path, path) + return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path)) } return true, nil } @@ -73,7 +73,7 @@ func isTemplate(arg string) (bool, error) { return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1]) } if path := strings.TrimLeft(arg, "/"); path != arg && path != "" { - return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, path) + return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg)) } return false, nil } @@ -82,6 +82,14 @@ func isJSONStart(arg string) bool { return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) } +// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it. +func pathAdvice(path, refusal string) error { + if err := checkPathNames(path); err != nil { + return err + } + return fmt.Errorf("%s; write %s", refusal, path) +} + // loneReference is the path a template spells when the value it holds — a format string, a // JSON string, or an object holding only a format — is one reference token and nothing else. func loneReference(arg string) (string, bool) { @@ -101,6 +109,9 @@ func loneReference(arg string) (string, bool) { if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") { return "", false } + if strings.HasPrefix(body, "/") { + body = "/" + strings.TrimLeft(body, "/") + } _, path, err := refShape(body) return path, err == nil } diff --git a/node.go b/node.go index 2a44c01..56f84a5 100644 --- a/node.go +++ b/node.go @@ -396,6 +396,16 @@ func checkName(name string) error { return nil } +// checkPathNames rejects a dotted path with a segment no name may be. +func checkPathNames(path string) error { + for _, seg := range strings.Split(path, ".") { + if err := checkName(seg); err != nil { + return fmt.Errorf("path %w", err) + } + } + return nil +} + // isOption reports whether a template key configures the node instead of naming a // field. These names can never be fields. func isOption(name string) bool { diff --git a/struct.go b/struct.go index 692bda1..727ae75 100644 --- a/struct.go +++ b/struct.go @@ -36,9 +36,8 @@ type structResult struct { err error } -// maxStructRecords caps the records one struct type fills, which pointers between struct -// types multiply along every path. -const maxStructRecords = 1 << 10 +// maxStructs caps the structs compiling one type walks through its fields. +const maxStructs = 1 << 10 // structShapeOf compiles a struct type once and remembers the answer. Callers hold the // generator's lock. @@ -50,7 +49,7 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { if label == "" { label = "struct" } - sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, records: maxStructRecords} + sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, structs: maxStructs} shape, err := sc.record(t, label) if err == nil && shape.empty() { err = fmt.Errorf("%s has no fake tags, so nothing to fill", t) @@ -79,13 +78,13 @@ type nestedStruct struct { func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 } -// structCompile is what compiling one struct type shares across the records it reaches: the +// structCompile is what compiling one struct type shares across the structs it reaches: the // loaded tree, the types compiling or embedded above, so a pointer back to one is left alone -// rather than filled without end, and how many more records it may build. +// rather than filled without end, and how many more structs it may walk. type structCompile struct { root map[string]node visiting map[reflect.Type]bool - records int + structs int } // structFields gathers what one struct type fills: its tagged fields, those its embedded @@ -100,8 +99,8 @@ type structFields struct { } func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) { - if sc.records--; sc.records < 0 { - return nil, fmt.Errorf(`%s: the struct fields reach more than %d records; leave a pointer unfilled with fake:"-"`, label, maxStructRecords) + if err := sc.spend(label); err != nil { + return nil, err } sc.visiting[t] = true defer delete(sc.visiting, t) @@ -117,6 +116,13 @@ func (sc *structCompile) record(t reflect.Type, label string) (*structShape, err return c.shape, nil } +func (sc *structCompile) spend(label string) error { + if sc.structs--; sc.structs >= 0 { + return nil + } + return fmt.Errorf(`%s: the struct fields reach more than %d structs; leave a struct field unfilled with fake:"-"`, label, maxStructs) +} + // walk gathers the fields of struct type t, which sits at index within c.t. func (c *structFields) walk(t reflect.Type, index []int) error { for i := 0; i < t.NumField(); i++ { @@ -184,6 +190,9 @@ func fieldPath(t reflect.Type, index []int) string { } func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { + if err := c.spend(c.label + "." + fieldPath(c.t, sf.Index)); err != nil { + return err + } c.visiting[elem] = true defer delete(c.visiting, elem) tags, nested := len(c.tags), len(c.shape.nested) @@ -218,10 +227,8 @@ func tagValue(sf reflect.StructField, tag string) (any, error) { case inline: return inputValue(tag) } - for _, seg := range strings.Split(tag, ".") { - if err := checkName(seg); err != nil { - return nil, fmt.Errorf("path %w", err) - } + if err := checkPathNames(tag); err != nil { + return nil, err } return "{/" + tag + "}", nil }