129 lines
3.5 KiB
Go
129 lines
3.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
var (
|
|
errInvalidDestination = errors.New("configkit: destination must contain only non-reserved mapping keys")
|
|
errDestinationLimit = errors.New("configkit: destination exceeds document limits")
|
|
)
|
|
|
|
func normalizeDestination(path Path) (Path, error) {
|
|
if !path.valid() {
|
|
return Path{}, errInvalidDestination
|
|
}
|
|
|
|
segments := make([]pathSegment, 0, len(path.segments))
|
|
for _, segment := range path.segments {
|
|
if segment.kind != pathSegmentKey && segment.kind != pathSegmentParsed {
|
|
return Path{}, errInvalidDestination
|
|
}
|
|
if segment.key == "configkit" || segment.key == "extends" {
|
|
return Path{}, errInvalidDestination
|
|
}
|
|
segments = append(segments, pathSegment{kind: pathSegmentKey, key: segment.key})
|
|
}
|
|
return Path{segments: segments}, nil
|
|
}
|
|
|
|
// appendDestination combines validated mapping-key paths with owned segments.
|
|
func appendDestination(parent, relative Path) Path {
|
|
segments := clonePathSegments(parent.segments, len(relative.segments))
|
|
segments = append(segments, relative.segments...)
|
|
return Path{segments: segments}
|
|
}
|
|
|
|
func invalidDestinationDiagnostic(destination Path) Diagnostic {
|
|
return Diagnostic{
|
|
Code: CodeOptionsInvalid, Severity: SeverityError,
|
|
Path: destination.clone(),
|
|
Message: "input destination must contain only non-reserved mapping keys",
|
|
}
|
|
}
|
|
|
|
// placeDocument checks all per-document bounds before allocating destination
|
|
// wrappers. Aggregate accounting remains the caller's responsibility.
|
|
func placeDocument(body *document.Node, destination Path, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) {
|
|
normalized, err := normalizeDestination(destination)
|
|
if err != nil {
|
|
return Path{}, nil, err
|
|
}
|
|
if body == nil {
|
|
return normalized, nil, nil
|
|
}
|
|
if !destinationWithinLimits(body, normalized, maxDepth, maxNodes) {
|
|
return normalized, nil, errDestinationLimit
|
|
}
|
|
|
|
placed := body
|
|
for index := len(normalized.segments) - 1; index >= 0; index-- {
|
|
key := normalized.segments[index].key
|
|
placed = &document.Node{
|
|
Kind: document.Mapping,
|
|
Location: sourceRoot,
|
|
Mapping: []document.Entry{{
|
|
Key: key,
|
|
KeyLocation: sourceRoot,
|
|
ValueLocation: sourceRoot,
|
|
Value: placed,
|
|
}},
|
|
}
|
|
}
|
|
return normalized, placed, nil
|
|
}
|
|
|
|
// destinationWithinLimits expects a normalized mapping-key destination.
|
|
func destinationWithinLimits(body *document.Node, destination Path, maxDepth, maxNodes int) bool {
|
|
if body == nil {
|
|
return true
|
|
}
|
|
if maxDepth < 0 || maxNodes < 0 || !destination.valid() {
|
|
return false
|
|
}
|
|
segments := len(destination.segments)
|
|
if segments > maxDepth || segments > maxNodes/2 {
|
|
return false
|
|
}
|
|
wrapperNodes := destinationWrapperNodeCount(destination)
|
|
for _, segment := range destination.segments {
|
|
if segment.kind != pathSegmentKey {
|
|
return false
|
|
}
|
|
}
|
|
if documentDepth(body) > maxDepth-segments {
|
|
return false
|
|
}
|
|
return countInputNodes(body) <= maxNodes-wrapperNodes
|
|
}
|
|
|
|
func destinationWrapperNodeCount(destination Path) int {
|
|
return 2 * len(destination.segments)
|
|
}
|
|
|
|
func documentDepth(node *document.Node) int {
|
|
if node == nil {
|
|
return -1
|
|
}
|
|
depth := 0
|
|
var visit func(*document.Node, int)
|
|
visit = func(current *document.Node, currentDepth int) {
|
|
if current == nil {
|
|
return
|
|
}
|
|
if currentDepth > depth {
|
|
depth = currentDepth
|
|
}
|
|
for _, entry := range current.Mapping {
|
|
visit(entry.Value, currentDepth+1)
|
|
}
|
|
for _, child := range current.Sequence {
|
|
visit(child, currentDepth+1)
|
|
}
|
|
}
|
|
visit(node, 0)
|
|
return depth
|
|
}
|