224 lines
7.5 KiB
Go
224 lines
7.5 KiB
Go
package configkit
|
|
|
|
import (
|
|
"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
|
|
}
|
|
|
|
func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) ([]preparedOverride, []SourceInfo, []Diagnostic) {
|
|
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)
|
|
}
|
|
|
|
return prepared, 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 == directiveKey {
|
|
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 == directiveKey {
|
|
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
|
|
}
|