460 lines
17 KiB
Go
460 lines
17 KiB
Go
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
|
|
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
|
|
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)
|
|
}
|
|
}
|
|
result.overlays = append(result.overlays, overlayRecord{change: Change{
|
|
Path: path.clone(), Origin: overlayOrigin(LayerCLI, location), Previous: previousOrigins(previous),
|
|
}, displaced: displaced})
|
|
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
|
|
}
|