package engine import ( "cmp" "slices" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type inheritanceWalker struct { root **document.Node limits projectionLimits transfers []inheritanceTransfer controls []inheritanceControl } // expandInheritanceTree resolves each destination scope after its enclosing // relationships. It remains a private, test-only operation until the loading // lifecycle also carries its complete provenance and inspection evidence. func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) { if input == nil || input.Kind != document.Mapping { return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError, Message: "inheritance requires a mapping document root"}} } root := input walker := inheritanceWalker{root: &root, limits: limits} 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, transfers: walker.transfers, controls: walker.controls}, nil } func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool) *Diagnostic { switch (*node).Kind { case document.Mapping: 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, }) *node = withoutInheritanceDirective(*node) } } resolved, diagnostics := expandSiblingDefinitionsWithLimits(*node, path, w.limits) if len(diagnostics) != 0 { return &diagnostics[0] } *node = resolved.root w.transfers = append(w.transfers, resolved.transfers...) w.controls = append(w.controls, resolved.controls...) if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) { problem := inheritanceBudgetDiagnostic(path, (*node).Location) return &problem } 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: 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 } // 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, } }