310 lines
10 KiB
Go
310 lines
10 KiB
Go
package engine
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type inheritanceWalker struct {
|
|
nodes int // logical non-directive nodes in the entire working tree
|
|
origins map[string]Origin
|
|
limits projectionLimits
|
|
retained *retainedNodeCounter
|
|
transfers []inheritanceTransfer
|
|
controls []inheritanceControl
|
|
replacements []inheritanceReplacement
|
|
operations []inheritanceOperation
|
|
}
|
|
|
|
// expandInheritanceTree resolves each destination scope after its enclosing
|
|
// relationships. The loading lifecycle uses the origin-aware entry point.
|
|
func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
|
return expandInheritanceWithOrigins(input, nil, limits)
|
|
}
|
|
|
|
func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
|
return expandInheritanceWithBudgets(input, inputOrigins, limits, historyNodeLimit)
|
|
}
|
|
|
|
func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) {
|
|
if input == nil || input.Kind != document.Mapping {
|
|
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
|
Message: "inheritance requires a mapping document root"}}
|
|
}
|
|
root := document.Clone(input)
|
|
origins := cloneOriginIndex(inputOrigins)
|
|
if inputOrigins == nil {
|
|
origins = directInheritanceOrigins(input, Path{}, nil)
|
|
}
|
|
nodes, within := inheritanceLowerBoundCount(root, limits.nodes)
|
|
if !within {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(Path{}, root.Location)}
|
|
}
|
|
walker := inheritanceWalker{
|
|
nodes: nodes, limits: limits, origins: origins,
|
|
retained: newRetainedNodeCounter(historyLimit),
|
|
}
|
|
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
|
return nil, []Diagnostic{*problem}
|
|
}
|
|
if problem := inheritanceFinalBudget(root, limits); problem != nil {
|
|
return nil, []Diagnostic{*problem}
|
|
}
|
|
return &inheritanceScope{
|
|
root: root, origins: walker.origins, transfers: walker.transfers,
|
|
controls: walker.controls, replacements: walker.replacements, operations: walker.operations,
|
|
}, nil
|
|
}
|
|
|
|
func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool) *Diagnostic {
|
|
switch (*node).Kind {
|
|
case document.Mapping:
|
|
original := *node
|
|
if unscoped {
|
|
reference, found := inheritanceDirective(*node)
|
|
if found {
|
|
referencePath := path.Key(inheritanceKey)
|
|
location := publicLocation(reference.Value.Location)
|
|
if reference.Value.Kind != document.Null {
|
|
problem := inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
|
|
"inheritance reference has no enclosing sibling scope")
|
|
return &problem
|
|
}
|
|
w.controls = append(w.controls, inheritanceControl{
|
|
path: referencePath, locations: []Location{location}, cancelled: true,
|
|
})
|
|
origin := directInheritanceOrigins(reference.Value, referencePath, w.origins)[referencePath.String()]
|
|
control := inheritanceControl{path: referencePath, locations: []Location{location}, cancelled: true}
|
|
w.operations = append(w.operations, inheritanceOperation{control: &control, controlOrigin: origin})
|
|
deleteOriginSubtree(w.origins, referencePath, reference.Value)
|
|
*node = withoutInheritanceDirective(*node)
|
|
}
|
|
}
|
|
if scopeHasInheritanceDefinitions(*node) {
|
|
// Only this scope can change the logical total. Reserve the nodes
|
|
// outside it so expansion checks the remaining budget before cloning.
|
|
before, _ := inheritanceLowerBoundCount(*node, -1)
|
|
limits := w.limits
|
|
if limits.nodes >= 0 {
|
|
limits.nodes -= w.nodes - before
|
|
}
|
|
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, limits, w.origins, w.retained)
|
|
if len(diagnostics) != 0 {
|
|
return &diagnostics[0]
|
|
}
|
|
*node = resolved.root
|
|
deleteOriginSubtree(w.origins, path, original)
|
|
for pointer, origin := range resolved.origins {
|
|
w.origins[pointer] = origin
|
|
}
|
|
w.transfers = append(w.transfers, resolved.transfers...)
|
|
w.controls = append(w.controls, resolved.controls...)
|
|
w.replacements = append(w.replacements, resolved.replacements...)
|
|
w.operations = append(w.operations, resolved.operations...)
|
|
after, _ := inheritanceLowerBoundCount(*node, -1)
|
|
w.nodes += after - before
|
|
}
|
|
// Operations retain immutable enclosing templates. Detach just this
|
|
// container before replacing descendants during destination resolution;
|
|
// untouched subtrees remain shared with the retained evidence.
|
|
working := **node
|
|
working.Mapping = slices.Clone(working.Mapping)
|
|
*node = &working
|
|
indices := make([]int, len((*node).Mapping))
|
|
for index := range indices {
|
|
indices[index] = index
|
|
}
|
|
slices.SortFunc(indices, func(left, right int) int {
|
|
return cmp.Compare((*node).Mapping[left].Key, (*node).Mapping[right].Key)
|
|
})
|
|
for _, index := range indices {
|
|
entry := &(*node).Mapping[index]
|
|
if problem := w.visit(&entry.Value, path.Key(entry.Key), false); problem != nil {
|
|
return problem
|
|
}
|
|
}
|
|
case document.Sequence:
|
|
working := **node
|
|
working.Sequence = slices.Clone(working.Sequence)
|
|
*node = &working
|
|
for index := range (*node).Sequence {
|
|
child := &(*node).Sequence[index]
|
|
if problem := w.visit(child, path.Index(index), (*child).Kind == document.Mapping); problem != nil {
|
|
return problem
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scopeHasInheritanceDefinitions(scope *document.Node) bool {
|
|
for _, entry := range scope.Mapping {
|
|
if entry.Value.Kind != document.Mapping {
|
|
continue
|
|
}
|
|
if _, found := inheritanceDirective(entry.Value); found {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// A directive may disappear during later destination resolution, so only
|
|
// non-directive nodes form a safe early lower bound on final output size.
|
|
func inheritanceLowerBoundWithin(root *document.Node, limit int) bool {
|
|
_, within := inheritanceLowerBoundCount(root, limit)
|
|
return within
|
|
}
|
|
|
|
func inheritanceLowerBoundCount(root *document.Node, limit int) (int, bool) {
|
|
if limit < 0 {
|
|
limit = int(^uint(0) >> 1)
|
|
}
|
|
stack := []*document.Node{root}
|
|
count := 0
|
|
for len(stack) != 0 {
|
|
node := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if node == nil {
|
|
continue
|
|
}
|
|
count++
|
|
if count > limit {
|
|
return count, false
|
|
}
|
|
switch node.Kind {
|
|
case document.Mapping:
|
|
for _, entry := range node.Mapping {
|
|
if entry.Key == inheritanceKey {
|
|
continue
|
|
}
|
|
count++ // mapping key
|
|
stack = append(stack, entry.Value)
|
|
}
|
|
case document.Sequence:
|
|
stack = append(stack, node.Sequence...)
|
|
}
|
|
if count > limit {
|
|
return count, false
|
|
}
|
|
}
|
|
return count, true
|
|
}
|
|
|
|
// Count the materialized mapping shape before cloning a memoized parent.
|
|
// Relationship fields are excluded because later destination resolution
|
|
// consumes them; their omission makes this a safe lower bound.
|
|
func inheritanceOverlayLowerBoundWithin(lower, upper *document.Node, limit int) bool {
|
|
if limit < 0 {
|
|
return false
|
|
}
|
|
index := new(mappingIndex)
|
|
count := 0
|
|
var visit func(*document.Node, *document.Node) bool
|
|
visit = func(base, incoming *document.Node) bool {
|
|
if base.Kind != document.Mapping || incoming.Kind != document.Mapping {
|
|
added, within := inheritanceLowerBoundCount(incoming, limit-count)
|
|
count += added
|
|
return within
|
|
}
|
|
count++ // mapping node
|
|
if count > limit {
|
|
return false
|
|
}
|
|
for _, entry := range base.Mapping {
|
|
if entry.Key == inheritanceKey {
|
|
continue
|
|
}
|
|
count++ // mapping key
|
|
if count > limit {
|
|
return false
|
|
}
|
|
position := index.find(incoming, entry.Key)
|
|
if position >= 0 {
|
|
if !visit(entry.Value, incoming.Mapping[position].Value) {
|
|
return false
|
|
}
|
|
} else {
|
|
added, within := inheritanceLowerBoundCount(entry.Value, limit-count)
|
|
count += added
|
|
if !within {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
for _, entry := range incoming.Mapping {
|
|
if entry.Key == inheritanceKey || index.find(base, entry.Key) >= 0 {
|
|
continue
|
|
}
|
|
count++ // mapping key
|
|
if count > limit {
|
|
return false
|
|
}
|
|
added, within := inheritanceLowerBoundCount(entry.Value, limit-count)
|
|
count += added
|
|
if !within || count > limit {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return visit(lower, upper)
|
|
}
|
|
|
|
func inheritanceFinalBudget(root *document.Node, limits projectionLimits) *Diagnostic {
|
|
type pending struct {
|
|
node *document.Node
|
|
path Path
|
|
}
|
|
stack := []pending{{node: root}}
|
|
count := 0
|
|
for len(stack) != 0 {
|
|
item := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if len(item.path.segments) > limits.depth {
|
|
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
|
|
return &problem
|
|
}
|
|
count++
|
|
if limits.nodes >= 0 && count > limits.nodes {
|
|
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
|
|
return &problem
|
|
}
|
|
switch item.node.Kind {
|
|
case document.Mapping:
|
|
count += len(item.node.Mapping) // mapping keys
|
|
for index := len(item.node.Mapping) - 1; index >= 0; index-- {
|
|
entry := item.node.Mapping[index]
|
|
stack = append(stack, pending{node: entry.Value, path: item.path.Key(entry.Key)})
|
|
}
|
|
case document.Sequence:
|
|
for index := len(item.node.Sequence) - 1; index >= 0; index-- {
|
|
stack = append(stack, pending{node: item.node.Sequence[index], path: item.path.Index(index)})
|
|
}
|
|
}
|
|
if limits.nodes >= 0 && count > limits.nodes {
|
|
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
|
|
return &problem
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func inheritanceBudgetDiagnostic(path Path, location document.Location) Diagnostic {
|
|
primary := publicLocation(location)
|
|
return Diagnostic{
|
|
Code: CodeSchemaProjectionLimit, Severity: SeverityError, Path: path,
|
|
Message: "expanded configuration exceeds the projection limit", Primary: &primary,
|
|
}
|
|
}
|
|
|
|
func inheritanceHistoryBudgetDiagnostic(path Path, location document.Location) Diagnostic {
|
|
primary := publicLocation(location)
|
|
return Diagnostic{
|
|
Code: CodeExplainLimitExceeded, Severity: SeverityError, Path: path,
|
|
Message: "retained inheritance history exceeds the node limit", Primary: &primary,
|
|
}
|
|
}
|