package configkit import ( "math" "reflect" "slices" "strconv" "strings" "unicode" "unicode/utf8" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) const maxOverrides = 1024 type preparedOverride struct { path Path value *document.Node source SourceInfo bytes int64 nodes int } type overrideValueBuilder struct { sourceID string active map[valueVisit]bool bytes int64 nodes int } func applyOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { if len(overrides) == 0 { return base, cloneSourceInfos(sources), nil } if len(overrides) > maxOverrides || len(sources)+len(overrides) > limits.sources { return nil, cloneSourceInfos(sources), []Diagnostic{{ Code: CodeOverrideLimitExceeded, Severity: SeverityError, Message: "configuration overrides exceed the record or source-count limit", }} } var diagnostics []Diagnostic for index, override := range overrides { if !validOverrideLabel(override.Flag) { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideInvalidLabel, Severity: SeverityError, Message: "override record " + strconv.Itoa(index+1) + " requires a safe nonempty flag name", }) } } if len(diagnostics) != 0 { return nil, cloneSourceInfos(sources), canonicalDiagnostics(diagnostics) } prepared := make([]preparedOverride, len(overrides)) allSources := cloneSourceInfos(sources) for index, override := range overrides { sourceID := "source-" + strconv.Itoa(len(sources)+index+1) info := SourceInfo{ID: sourceID, Label: override.Flag, Layer: LayerCLI} allSources = append(allSources, info) prepared[index].source = info } seen := make(map[string]int, len(overrides)) pathAccepted := make([]bool, len(overrides)) var usedBytes int64 usedNodes := 0 for index, override := range overrides { location := &Location{SourceID: prepared[index].source.ID} if issue := validateOverridePath(override.Path, base.root); issue != nil { issue.Primary = location diagnostics = append(diagnostics, *issue) continue } pointer := override.Path.String() if previous, duplicate := seen[pointer]; duplicate { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideDuplicate, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration override path is repeated", Primary: location, Related: []Location{{SourceID: prepared[previous].source.ID}}, }) continue } seen[pointer] = index if conflicting := overlappingOverride(overrides[:index], pathAccepted[:index], override.Path); conflicting >= 0 { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideOverlap, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration override paths overlap", Primary: location, Related: []Location{{SourceID: prepared[conflicting].source.ID}}, }) continue } if rule != nil && (pathContains(override.Path, rule.Path) || pathContains(rule.Path, override.Path)) { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideVersion, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration overrides cannot replace the schema-version path", Primary: location, }) continue } if pathContainsDirective(override.Path) { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideDirective, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration overrides cannot create configuration directives", Primary: location, }) continue } pathAccepted[index] = true builder := overrideValueBuilder{sourceID: prepared[index].source.ID, active: make(map[valueVisit]bool)} value, issue := builder.build(override.Value, override.Path, len(override.Path.segments)) if issue != nil { issue.Primary = location diagnostics = append(diagnostics, *issue) continue } if relative, ok := overrideDirectivePath(value, Path{}); ok { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideDirective, Severity: SeverityError, Path: appendOverridePath(override.Path, relative), Message: "configuration overrides cannot create configuration directives", Primary: location, }) continue } nodes := builder.nodes + len(override.Path.segments)*2 if nodes > limits.nodes || base.inputNodes+usedNodes+nodes > limits.nodes { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideLimitExceeded, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration overrides exceed the aggregate node limit", Primary: location, }) continue } if builder.bytes > maxSourceBytes || base.inputBytes+usedBytes+builder.bytes > limits.bytes { diagnostics = append(diagnostics, Diagnostic{ Code: CodeOverrideLimitExceeded, Severity: SeverityError, Path: override.Path.clone(), Message: "configuration overrides exceed a record or aggregate string-byte limit", Primary: location, }) continue } prepared[index].path = override.Path.clone() prepared[index].value = value prepared[index].bytes = builder.bytes prepared[index].nodes = nodes usedBytes += builder.bytes usedNodes += nodes } if len(diagnostics) != 0 { return nil, allSources, canonicalDiagnostics(diagnostics) } result := cloneComposedDocument(base) for _, override := range prepared { applyPreparedOverride(result, override) } result.inputBytes += usedBytes result.inputNodes += usedNodes return result, allSources, nil } func validOverrideLabel(label string) bool { if label == "" || strings.TrimSpace(label) != label || !utf8.ValidString(label) { return false } for _, character := range label { if unicode.IsControl(character) { return false } } return true } func validateOverridePath(path Path, root *document.Node) *Diagnostic { if !path.valid() || len(path.segments) == 0 { return &Diagnostic{Code: CodeOverrideInvalidPath, Severity: SeverityError, Path: path.clone(), Message: "configuration override path must be a valid non-root mapping path"} } current := root for index, segment := range path.segments { if segment.kind == pathSegmentIndex { return &Diagnostic{Code: CodeOverrideInvalidPath, Severity: SeverityError, Path: path.clone(), Message: "configuration override paths cannot contain sequence indices"} } if current == nil { continue } if current.Kind != document.Mapping { return &Diagnostic{Code: CodeOverrideInvalidPath, Severity: SeverityError, Path: path.clone(), Message: "configuration override path traverses a non-mapping value"} } position := documentEntryIndex(current.Mapping, segment.key) if index == len(path.segments)-1 { continue } if position < 0 { current = nil continue } current = current.Mapping[position].Value } return nil } func overlappingOverride(previous []Override, valid []bool, path Path) int { for index, override := range previous { if valid[index] && (pathContains(override.Path, path) || pathContains(path, override.Path)) { return index } } return -1 } func pathContainsDirective(path Path) bool { for _, segment := range path.segments { if segment.key == "_configkit" { return true } } return false } func appendOverridePath(base, relative Path) Path { result := base.clone() result.segments = append(result.segments, clonePathSegments(relative.segments, 0)...) return result } func overrideDirectivePath(node *document.Node, path Path) (Path, bool) { if node.Kind == document.Mapping { for _, entry := range node.Mapping { child := path.Key(entry.Key) if entry.Key == "_configkit" { return child, true } if found, ok := overrideDirectivePath(entry.Value, child); ok { return found, true } } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { if found, ok := overrideDirectivePath(child, path.Index(index)); ok { return found, true } } } return Path{}, false } 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} } func cloneComposedDocument(base *composedDocument) *composedDocument { return &composedDocument{ root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims), origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays), applications: cloneHistoryApplications(base.applications), inputBytes: base.inputBytes, inputNodes: base.inputNodes, } } func applyPreparedOverride(result *composedDocument, override preparedOverride) { location := document.Location{SourceID: override.source.ID} mergeOverlayOrigin(result.origins, Path{}, location, LayerCLI) current := result.root currentPath := Path{} for index, segment := range override.path.segments { path := currentPath.Key(segment.key) position := documentEntryIndex(current.Mapping, segment.key) if index < len(override.path.segments)-1 { if position < 0 { child := &document.Node{Kind: document.Mapping, Location: location, Mapping: []document.Entry{}} current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: child}) collectOverlayOrigins(result.origins, child, path, LayerCLI) current = child } else { current = current.Mapping[position].Value mergeOverlayOrigin(result.origins, path, location, LayerCLI) } currentPath = path continue } previous := cloneOrigin(result.origins[path.String()]) var displaced []displacedDefinition var before *document.Node if position < 0 { incoming := document.Clone(override.value) current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: incoming}) collectOverlayOrigins(result.origins, incoming, path, LayerCLI) } else { existing := current.Mapping[position].Value before = document.Clone(existing) displaced = affectedOverrideOrigins(existing, override.value, result.origins, path) if existing.Kind == document.Mapping && override.value.Kind == document.Mapping { mergeOverlayOrigin(result.origins, path, location, LayerCLI) mergeOverlayMappings(result, existing, override.value, path, LayerCLI, false) } else { deleteOriginSubtree(result.origins, path) current.Mapping[position] = document.Entry{Key: segment.key, KeyLocation: location, ValueLocation: location, Value: document.Clone(override.value)} collectOverlayOrigins(result.origins, current.Mapping[position].Value, path, LayerCLI) } } position = documentEntryIndex(current.Mapping, segment.key) result.overlays = append(result.overlays, overlayRecord{change: Change{ Path: path.clone(), Origin: overlayOrigin(LayerCLI, location), Previous: previousOrigins(previous), }, displaced: displaced, before: before, after: document.Clone(current.Mapping[position].Value)}) result.applications = append(result.applications, historyApplication{ layer: LayerCLI, path: path.clone(), value: override.value, origin: overlayOrigin(LayerCLI, location), }) } } func affectedOverrideOrigins(existing, incoming *document.Node, origins map[string]Origin, path Path) []displacedDefinition { displaced := []displacedDefinition{{path: path.clone(), origin: cloneOrigin(origins[path.String()])}} if existing.Kind == document.Mapping && incoming.Kind == document.Mapping { for _, entry := range incoming.Mapping { position := documentEntryIndex(existing.Mapping, entry.Key) if position < 0 { continue } childPath := path.Key(entry.Key) childExisting := existing.Mapping[position].Value if childExisting.Kind == document.Mapping && entry.Value.Kind == document.Mapping { displaced = append(displaced, affectedOverrideOrigins(childExisting, entry.Value, origins, childPath)...) } else { displaced = append(displaced, displacedOrigins(childExisting, origins, childPath)...) } } } else { displaced = displacedOrigins(existing, origins, path) } slices.SortFunc(displaced, func(left, right displacedDefinition) int { return strings.Compare(left.path.String(), right.path.String()) }) return displaced }