package configkit import ( "math" "reflect" "slices" "strconv" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type overrideValueBuilder struct { sourceID string active map[valueVisit]bool bytes int64 nodes int } func (b *overrideValueBuilder) build(value any, path Path, depth int) (*document.Node, *Diagnostic) { if depth > maxDocumentDepth { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the depth limit") } b.nodes++ if b.nodes > maxDocumentNodes { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit") } location := document.Location{SourceID: b.sourceID} switch value := value.(type) { case nil: return &document.Node{Kind: document.Null, Location: location}, nil case string: if !b.addBytes(len(value)) { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the record string-byte limit") } return &document.Node{Kind: document.String, Text: value, Location: location}, nil case bool: return &document.Node{Kind: document.Boolean, Text: strconv.FormatBool(value), Location: location}, nil case int: return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil case int8: return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil case int16: return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil case int32: return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil case int64: return overrideNumber(strconv.FormatInt(value, 10), location), nil case uint: return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil case uint8: return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil case uint16: return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil case uint32: return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil case uint64: return overrideNumber(strconv.FormatUint(value, 10), location), nil case float32: if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) { return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite") } return overrideNumber(strconv.FormatFloat(float64(value), 'g', -1, 32), location), nil case float64: if math.IsNaN(value) || math.IsInf(value, 0) { return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite") } return overrideNumber(strconv.FormatFloat(value, 'g', -1, 64), location), nil case []any: if value == nil { return &document.Node{Kind: document.Null, Location: location}, nil } leave, diagnostic := b.enter(reflect.ValueOf(value), path) if diagnostic != nil { return nil, diagnostic } defer leave() if len(value) > maxDocumentNodes-b.nodes { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit") } node := &document.Node{Kind: document.Sequence, Location: location, Sequence: make([]*document.Node, len(value))} for index, child := range value { built, diagnostic := b.build(child, path.Index(index), depth+1) if diagnostic != nil { return nil, diagnostic } node.Sequence[index] = built } return node, nil case map[string]any: if value == nil { return &document.Node{Kind: document.Null, Location: location}, nil } leave, diagnostic := b.enter(reflect.ValueOf(value), path) if diagnostic != nil { return nil, diagnostic } defer leave() if len(value) > (maxDocumentNodes-b.nodes)/2 { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit") } keys := make([]string, 0, len(value)) for key := range value { keys = append(keys, key) } slices.Sort(keys) node := &document.Node{Kind: document.Mapping, Location: location, Mapping: make([]document.Entry, 0, len(keys))} for _, key := range keys { b.nodes++ // mapping key if !b.addBytes(len(key)) { return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path.Key(key), "configuration override exceeds the record string-byte limit") } built, diagnostic := b.build(value[key], path.Key(key), depth+1) if diagnostic != nil { return nil, diagnostic } node.Mapping = append(node.Mapping, document.Entry{Key: key, KeyLocation: location, ValueLocation: location, Value: built}) } return node, nil default: return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override value has an unsupported type") } } func (b *overrideValueBuilder) addBytes(count int) bool { if count < 0 || int64(count) > maxSourceBytes-b.bytes { return false } b.bytes += int64(count) return true } func (b *overrideValueBuilder) enter(value reflect.Value, path Path) (func(), *Diagnostic) { visit, ok := visitFor(value) if !ok { return func() {}, nil } if b.active[visit] { return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override values must not contain cycles") } b.active[visit] = true return func() { delete(b.active, visit) }, nil } func overrideValueDiagnostic(code Code, path Path, message string) *Diagnostic { return &Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message} } func overrideNumber(text string, location document.Location) *document.Node { return &document.Node{Kind: document.Number, Text: text, Location: location} }