198 lines
6.1 KiB
Go
198 lines
6.1 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")
|
|
)
|
|
|
|
type placementMode uint8
|
|
|
|
const (
|
|
placementModeMapping placementMode = iota
|
|
placementModeSequenceItem
|
|
)
|
|
|
|
// PlacementModeFromFacade preserves root-owned numeric values until preflight
|
|
// can reject unsupported modes.
|
|
func PlacementModeFromFacade(value uint8) placementMode { return placementMode(value) }
|
|
|
|
// PlacementModeForFacade converts private source evidence to the root-owned mode.
|
|
func PlacementModeForFacade(value placementMode) uint8 { return uint8(value) }
|
|
|
|
// InputWithPlacement copies the root-owned mode into a private engine input.
|
|
func InputWithPlacement(input Input, value uint8) Input {
|
|
input.placementMode = placementMode(value)
|
|
return input
|
|
}
|
|
|
|
// OptionsWithPlacement copies root-owned controls into private engine options.
|
|
func OptionsWithPlacement[T any](options Options[T], destination Path, value uint8) Options[T] {
|
|
options.destination = destination.clone()
|
|
options.placementMode = placementMode(value)
|
|
return options
|
|
}
|
|
|
|
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) {
|
|
return placeDocumentWithMode(body, destination, placementModeMapping, sourceRoot, maxDepth, maxNodes)
|
|
}
|
|
|
|
// placeDocumentWithMode places a source mapping beneath its literal destination.
|
|
// Sequence-item placement adds one sequence containing the complete mapping.
|
|
func placeDocumentWithMode(body *document.Node, destination Path, mode placementMode, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) {
|
|
normalized, err := normalizeDestination(destination)
|
|
if err != nil {
|
|
return Path{}, nil, err
|
|
}
|
|
if mode != placementModeMapping && mode != placementModeSequenceItem {
|
|
return Path{}, nil, errInvalidDestination
|
|
}
|
|
if mode == placementModeSequenceItem && len(normalized.segments) == 0 {
|
|
return Path{}, nil, errInvalidDestination
|
|
}
|
|
if body == nil {
|
|
return normalized, nil, nil
|
|
}
|
|
if mode == placementModeSequenceItem && body.Kind != document.Mapping {
|
|
return normalized, nil, errInvalidDestination
|
|
}
|
|
if !destinationWithinLimitsWithMode(body, normalized, mode, maxDepth, maxNodes) {
|
|
return normalized, nil, errDestinationLimit
|
|
}
|
|
|
|
placed := body
|
|
if mode == placementModeSequenceItem {
|
|
placed = &document.Node{
|
|
Kind: document.Sequence, Location: sourceRoot,
|
|
Sequence: []*document.Node{placed},
|
|
}
|
|
}
|
|
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 {
|
|
return destinationWithinLimitsWithMode(body, destination, placementModeMapping, maxDepth, maxNodes)
|
|
}
|
|
|
|
func destinationWithinLimitsWithMode(body *document.Node, destination Path, mode placementMode, maxDepth, maxNodes int) bool {
|
|
if body == nil {
|
|
return true
|
|
}
|
|
if maxDepth < 0 || maxNodes < 0 || !destination.valid() || (mode != placementModeMapping && mode != placementModeSequenceItem) {
|
|
return false
|
|
}
|
|
segments := len(destination.segments)
|
|
sequenceNodes := 0
|
|
sequenceDepth := 0
|
|
if mode == placementModeSequenceItem {
|
|
if segments == 0 {
|
|
return false
|
|
}
|
|
sequenceNodes = 1
|
|
sequenceDepth = 1
|
|
}
|
|
if segments > maxDepth-sequenceDepth || segments > (maxNodes-sequenceNodes)/2 {
|
|
return false
|
|
}
|
|
wrapperNodes := destinationWrapperNodeCount(destination) + sequenceNodes
|
|
for _, segment := range destination.segments {
|
|
if segment.kind != pathSegmentKey {
|
|
return false
|
|
}
|
|
}
|
|
if documentDepth(body) > maxDepth-segments-sequenceDepth {
|
|
return false
|
|
}
|
|
return countInputNodes(body) <= maxNodes-wrapperNodes
|
|
}
|
|
|
|
func destinationWrapperNodeCountForMode(destination Path, mode placementMode) int {
|
|
count := destinationWrapperNodeCount(destination)
|
|
if mode == placementModeSequenceItem {
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
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
|
|
}
|