738 lines
31 KiB
Go
738 lines
31 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io/fs"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
|
)
|
|
|
|
const (
|
|
maxImportDepth = 64
|
|
maxImportSources = 1024
|
|
maxGlobEntries = 1_000_000
|
|
)
|
|
|
|
type importLimits struct {
|
|
depth int
|
|
sources int
|
|
bytes int64
|
|
nodes int
|
|
entries int
|
|
}
|
|
|
|
func defaultImportLimits() importLimits {
|
|
return importLimits{
|
|
depth: maxImportDepth, sources: maxImportSources,
|
|
bytes: maxTotalBytes, nodes: maxDocumentNodes, entries: maxGlobEntries,
|
|
}
|
|
}
|
|
|
|
type importRoute struct {
|
|
canonical string
|
|
locations []Location
|
|
}
|
|
|
|
type importAccounting struct {
|
|
sources int
|
|
bytes int64
|
|
nodes int
|
|
entries int
|
|
}
|
|
|
|
type importTraversal struct {
|
|
access *filesystemAccess
|
|
accounting *importAccounting
|
|
limits importLimits
|
|
rule *VersionRule
|
|
versionState *sourceVersionState
|
|
|
|
scope acquisitionScope
|
|
itemScope bool
|
|
itemPrefix Path
|
|
sources []SourceInfo
|
|
active map[resourceIdentity]int
|
|
visited map[resourceIdentity]importRoute
|
|
stack []importRoute
|
|
}
|
|
|
|
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
return loadImportedDocumentsTracked(source, context, main, Path{}, rule, limits, &importAccounting{}, nil, nil)
|
|
}
|
|
|
|
func loadImportedDocumentsTracked(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
return loadImportedDocumentsTrackedWithVersion(source, context, main, destination, rule, limits, accounting, visited, hostToken, nil)
|
|
}
|
|
|
|
func loadImportedDocumentsTrackedWithVersion(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
var processing *sourceVersionProcessingResult
|
|
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
|
processing = versionState.mainProcessing
|
|
}
|
|
return loadImportedDocumentsTrackedWithProcessing(source, context, main, destination, rule, limits, accounting, visited, hostToken, versionState, processing, sequencePolicy{})
|
|
}
|
|
|
|
func loadImportedDocumentsTrackedWithProcessing(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
if nativeContext(source, context) {
|
|
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
|
|
if diagnostic != nil {
|
|
return nil, sourceInfos(main.source), []Diagnostic{*diagnostic}
|
|
}
|
|
return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy)
|
|
}
|
|
rootPath, basePath, mainPath, diagnostic := importFilesystem(source, context, main)
|
|
if diagnostic != nil {
|
|
return nil, sourceInfos(main.source), []Diagnostic{*diagnostic}
|
|
}
|
|
access, err := openHostAccess(rootPath)
|
|
if err != nil {
|
|
diagnostic := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{})
|
|
diagnostic.Primary = nil
|
|
return nil, sourceInfos(main.source), []Diagnostic{diagnostic}
|
|
}
|
|
if hostToken != nil {
|
|
access.token = hostToken
|
|
}
|
|
return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy)
|
|
}
|
|
|
|
func loadImportedWithAccess(access *filesystemAccess, basePath, mainPath string, main parsedSource, rule *VersionRule, limits importLimits, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
return loadImportedWithAccessTracked(access, basePath, mainPath, main, Path{}, rule, limits, accounting, nil)
|
|
}
|
|
|
|
func loadImportedWithAccessTracked(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
return loadImportedWithAccessTrackedWithVersion(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, nil)
|
|
}
|
|
|
|
func loadImportedWithAccessTrackedWithVersion(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
var processing *sourceVersionProcessingResult
|
|
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
|
processing = versionState.mainProcessing
|
|
}
|
|
return loadImportedWithAccessTrackedAndProcessing(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing)
|
|
}
|
|
|
|
func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, sequencePolicy{})
|
|
}
|
|
|
|
func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) {
|
|
if !access.contains(basePath) || (mainPath != "" && !access.contains(mainPath)) {
|
|
_ = access.close()
|
|
return nil, sourceInfos(main.source), []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)}
|
|
}
|
|
for _, directory := range []string{access.rootPath, basePath} {
|
|
info, err := access.stat(directory)
|
|
if err != nil || !info.IsDir() {
|
|
_ = access.close()
|
|
return nil, sourceInfos(main.source), []Diagnostic{importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil)}
|
|
}
|
|
}
|
|
mainInfo := cloneSourceInfo(*main.source)
|
|
mainInfo.Destination = destination.clone()
|
|
mainIsItem := main.source.placementMode == placementModeSequenceItem
|
|
if mainIsItem {
|
|
mainInfo.destinationUnresolved = true
|
|
}
|
|
if mainPath != "" {
|
|
mainInfo.Path = mainPath
|
|
}
|
|
mainKey := access.identity(mainPath)
|
|
if mainPath == "" {
|
|
mainKey = access.syntheticIdentity()
|
|
}
|
|
if previous, repeated := visited[mainKey]; repeated {
|
|
_ = access.close()
|
|
return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})}
|
|
}
|
|
accounting.sources++
|
|
accounting.bytes += main.bytes
|
|
accounting.nodes += countInputNodes(main.document)
|
|
state := importTraversal{
|
|
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
|
|
scope: acquisitionScope{destination: destination.clone()},
|
|
sources: []SourceInfo{mainInfo},
|
|
active: make(map[resourceIdentity]int), visited: visited,
|
|
}
|
|
if mainIsItem {
|
|
state.itemScope = true
|
|
state.itemPrefix = destination.clone()
|
|
state.scope.destination = Path{}
|
|
state.sources[0] = cloneSourceInfo(mainInfo)
|
|
}
|
|
if state.visited == nil {
|
|
state.visited = make(map[resourceIdentity]importRoute)
|
|
}
|
|
if diagnostic := state.checkInitialLimits(); diagnostic != nil {
|
|
_ = access.close()
|
|
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
|
|
}
|
|
|
|
mainIdentity := mainPath
|
|
if mainIdentity == "" {
|
|
mainIdentity = "<selected-source>"
|
|
}
|
|
mainRoute := importRoute{canonical: mainIdentity}
|
|
state.active[mainKey] = 0
|
|
state.visited[mainKey] = mainRoute
|
|
state.stack = append(state.stack, mainRoute)
|
|
var body *document.Node
|
|
var declarations []importDeclaration
|
|
var diagnostics []Diagnostic
|
|
if processing != nil {
|
|
body = processing.payload
|
|
declarations = processing.imports
|
|
} else {
|
|
body, declarations, diagnostics = splitImportDirectives(main.document)
|
|
}
|
|
if len(diagnostics) == 0 {
|
|
if mainIsItem {
|
|
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
|
source: cloneSourceInfo(mainInfo), payload: body, document: body, processing: processing,
|
|
})
|
|
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
|
} else {
|
|
_, placed, err := placeDocument(body, destination, body.Location, maxDocumentDepth, maxDocumentNodes)
|
|
placementNodes := destinationWrapperNodeCount(destination)
|
|
if err != nil {
|
|
diagnostics = []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, main.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
|
} else if accounting.nodes+placementNodes > limits.nodes {
|
|
diagnostics = []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)}
|
|
} else {
|
|
accounting.nodes += placementNodes
|
|
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
|
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
|
|
destination: destination.clone(), processing: processing,
|
|
})
|
|
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
|
|
}
|
|
}
|
|
}
|
|
|
|
closeErr := access.close()
|
|
if len(diagnostics) != 0 {
|
|
return nil, cloneSourceInfos(state.sources), diagnostics
|
|
}
|
|
if closeErr != nil {
|
|
return nil, cloneSourceInfos(state.sources), []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)}
|
|
}
|
|
if mainIsItem {
|
|
localScope := state.scope
|
|
documents := make([]*document.Node, 0, len(localScope.contributions))
|
|
for _, contribution := range localScope.contributions {
|
|
documents = append(documents, contribution.document)
|
|
}
|
|
completed, failures := composeDocuments(documents...)
|
|
if len(failures) != 0 {
|
|
return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(0))
|
|
}
|
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)}
|
|
projectNestedEvidence(completed, localScope)
|
|
_, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
|
if err != nil {
|
|
return nil, cloneSourceInfos(state.sources), []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, main.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
|
}
|
|
placementNodes := destinationWrapperNodeCountForMode(destination, placementModeSequenceItem)
|
|
if accounting.nodes+placementNodes > limits.nodes {
|
|
return nil, cloneSourceInfos(state.sources), []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)}
|
|
}
|
|
accounting.nodes += placementNodes
|
|
composed, failures := composeDocumentsWithPolicy(policy, placed)
|
|
if composed != nil {
|
|
synthetic := acquisitionScope{contributions: []acquiredContribution{{
|
|
source: cloneSourceInfo(mainInfo), payload: completed.root, document: placed,
|
|
destination: destination.clone(), processing: processing, nested: completed,
|
|
}}}
|
|
projectNestedEvidence(composed, synthetic)
|
|
projectAcquiredDestinations(composed, synthetic, state.sources)
|
|
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
|
|
composed.inputBytes = accounting.bytes
|
|
composed.inputNodes = accounting.nodes
|
|
}
|
|
return composed, cloneSourceInfos(state.sources), failures
|
|
}
|
|
documents := make([]*document.Node, 0, len(state.scope.contributions))
|
|
for _, contribution := range state.scope.contributions {
|
|
documents = append(documents, contribution.document)
|
|
}
|
|
composed, diagnostics := composeDocumentsWithPolicy(policy, documents...)
|
|
if composed != nil {
|
|
projectNestedEvidence(composed, state.scope)
|
|
projectAcquiredDestinations(composed, state.scope, state.sources)
|
|
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
|
|
composed.inputBytes = accounting.bytes
|
|
composed.inputNodes = accounting.nodes
|
|
}
|
|
return composed, cloneSourceInfos(state.sources), diagnostics
|
|
}
|
|
|
|
func (s *importTraversal) checkInitialLimits() *Diagnostic {
|
|
if s.accounting.sources > s.limits.sources {
|
|
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil)
|
|
return &value
|
|
}
|
|
if s.accounting.bytes > s.limits.bytes {
|
|
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate byte limit", nil)
|
|
return &value
|
|
}
|
|
if s.accounting.nodes > s.limits.nodes {
|
|
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)
|
|
return &value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *importTraversal) walkDeclarations(basePath string, declarations []importDeclaration, depth int, parentDestination Path) []Diagnostic {
|
|
for _, declaration := range declarations {
|
|
childDestination := appendDestination(parentDestination, declaration.destination)
|
|
if declaration.glob != "" {
|
|
matches, diagnostics := s.expandImportGlob(basePath, declaration)
|
|
if len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
if len(matches) == 0 {
|
|
if declaration.optional {
|
|
continue
|
|
}
|
|
location := declaration.location
|
|
return []Diagnostic{importDiagnostic(CodeImportMissing, "required import glob matched no files", &location)}
|
|
}
|
|
for _, match := range matches {
|
|
matched := declaration
|
|
matched.path = match
|
|
matched.glob = ""
|
|
if diagnostics := s.visitPath(basePath, matched, childDestination, declaration.destination, depth+1); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if diagnostics := s.visitPath(basePath, declaration, childDestination, declaration.destination, depth+1); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *importTraversal) visitPath(basePath string, declaration importDeclaration, destination, relativeDestination Path, depth int) []Diagnostic {
|
|
location := declaration.location
|
|
if declaration.placement == placementModeSequenceItem && len(destination.segments) == 0 {
|
|
return []Diagnostic{{
|
|
Code: CodeImportInvalid, Severity: SeverityError,
|
|
Path: declaration.placementPath.clone(),
|
|
Message: "sequence-item placement requires a nonempty destination",
|
|
Primary: cloneLocation(declaration.placementLocation),
|
|
}}
|
|
}
|
|
if depth > s.limits.depth {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the active import-depth limit", &location)}
|
|
}
|
|
if !validImportPath(declaration.path) {
|
|
return []Diagnostic{importDiagnostic(CodeImportInvalid, "import path must be a nonempty slash-relative path without a network scheme", &location)}
|
|
}
|
|
|
|
candidate := s.access.join(basePath, declaration.path)
|
|
if !s.access.contains(candidate) {
|
|
return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target is outside the configuration root", &location)}
|
|
}
|
|
canonical, err := s.access.canonical(candidate)
|
|
if err != nil {
|
|
if declaration.optional && errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
code := CodeImportInvalid
|
|
message := "import target could not be canonicalized"
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
code, message = CodeImportMissing, "required import target does not exist"
|
|
} else if errors.Is(err, fs.ErrPermission) {
|
|
message = "import target cannot be accessed"
|
|
}
|
|
return []Diagnostic{importDiagnostic(code, message, &location)}
|
|
}
|
|
if !s.access.contains(canonical) {
|
|
return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target resolves outside the configuration root", &location)}
|
|
}
|
|
|
|
currentRoute := s.currentRoute(location)
|
|
identity := s.access.identity(canonical)
|
|
if activeIndex, ok := s.active[identity]; ok {
|
|
cycle := append([]importRoute(nil), s.stack[activeIndex:]...)
|
|
return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)}
|
|
}
|
|
if previous, ok := s.visited[identity]; ok {
|
|
return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)}
|
|
}
|
|
if s.accounting.sources+1 > s.limits.sources {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", &location)}
|
|
}
|
|
|
|
if _, err := s.access.relative(canonical); err != nil {
|
|
return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target could not be addressed inside the configuration root", &location)}
|
|
}
|
|
sourceID := "source-" + strconv.Itoa(s.accounting.sources+1)
|
|
kind := SourceKindFile
|
|
label := declaration.path
|
|
if s.access.native != nil {
|
|
kind = SourceKindFilesystem
|
|
label = s.access.label + "/" + canonical
|
|
}
|
|
info := SourceInfo{ID: sourceID, Label: label, Path: canonical, Destination: s.sourceDestination(destination), Layer: LayerBase, Kind: kind, ImportedFrom: &location}
|
|
if declaration.placement == placementModeSequenceItem {
|
|
info.placementMode = placementModeSequenceItem
|
|
}
|
|
info.destinationUnresolved = s.itemScope || declaration.placement == placementModeSequenceItem
|
|
if kind == SourceKindFilesystem {
|
|
info.namespace = s.access.token
|
|
}
|
|
s.sources = append(s.sources, info)
|
|
data, skipped, diagnostic := s.readImport(canonical, sourceID, declaration.optional)
|
|
if diagnostic != nil {
|
|
return []Diagnostic{*diagnostic}
|
|
}
|
|
if skipped {
|
|
s.sources = s.sources[:len(s.sources)-1]
|
|
return nil
|
|
}
|
|
s.accounting.sources++
|
|
|
|
root, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{
|
|
Depth: maxDocumentDepth, Nodes: maxDocumentNodes,
|
|
})
|
|
if len(issues) != 0 {
|
|
return parserDiagnostics(sourceID, issues)
|
|
}
|
|
nodeCount := countInputNodes(root)
|
|
if s.accounting.nodes+nodeCount > s.limits.nodes {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
|
}
|
|
s.accounting.nodes += nodeCount
|
|
|
|
var body *document.Node
|
|
var declarations []importDeclaration
|
|
var diagnostics []Diagnostic
|
|
var processing *sourceVersionProcessingResult
|
|
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
|
|
processed, failures := s.versionState.process(root, sourceVersionContribution, destination, sourceID)
|
|
if len(failures) != 0 {
|
|
return failures
|
|
}
|
|
processing = &processed
|
|
body, declarations = processed.payload, processed.imports
|
|
} else {
|
|
body, declarations, diagnostics = splitImportDirectives(root)
|
|
if len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
placementDestination := destination
|
|
mode := placementModeMapping
|
|
if declaration.placement == placementModeSequenceItem {
|
|
mode = placementModeSequenceItem
|
|
}
|
|
if mode == placementModeSequenceItem {
|
|
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
|
s.active[identity] = len(s.stack)
|
|
s.visited[identity] = route
|
|
s.stack = append(s.stack, route)
|
|
diagnostics := s.visitItemSource(canonical, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
|
|
delete(s.active, identity)
|
|
s.stack = s.stack[:len(s.stack)-1]
|
|
return diagnostics
|
|
}
|
|
_, placed, err := placeDocument(body, placementDestination, body.Location, maxDocumentDepth, maxDocumentNodes)
|
|
if err != nil {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &location)}
|
|
}
|
|
placementNodes := destinationWrapperNodeCount(placementDestination)
|
|
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
|
}
|
|
s.accounting.nodes += placementNodes
|
|
protectionRule := s.rule
|
|
if s.versionState != nil && s.versionState.effectiveRule != nil {
|
|
protectionRule = s.versionState.effectiveRule
|
|
}
|
|
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
|
|
versionLocation := publicLocation(versionNode.Location)
|
|
return []Diagnostic{{
|
|
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
|
|
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
|
|
}}
|
|
}
|
|
|
|
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
|
source: cloneSourceInfo(info), payload: body, document: placed,
|
|
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
|
})
|
|
s.active[identity] = len(s.stack)
|
|
s.visited[identity] = route
|
|
s.stack = append(s.stack, route)
|
|
diagnostics = s.walkDeclarations(s.access.dir(canonical), declarations, depth, destination)
|
|
delete(s.active, identity)
|
|
s.stack = s.stack[:len(s.stack)-1]
|
|
return diagnostics
|
|
}
|
|
|
|
func (s *importTraversal) sourceDestination(destination Path) Path {
|
|
if !s.itemScope {
|
|
return destination.clone()
|
|
}
|
|
return appendDestination(s.itemPrefix, destination)
|
|
}
|
|
|
|
func (s *importTraversal) visitItemSource(canonical string, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
|
|
parentScope := s.scope
|
|
parentItemScope, parentPrefix := s.itemScope, s.itemPrefix
|
|
s.scope = acquisitionScope{}
|
|
s.itemScope = true
|
|
if parentItemScope {
|
|
s.itemPrefix = appendDestination(parentPrefix, destination)
|
|
} else {
|
|
s.itemPrefix = destination.clone()
|
|
}
|
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
|
source: cloneSourceInfo(info), payload: body, document: body,
|
|
processing: processing,
|
|
})
|
|
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
|
itemScope := s.scope
|
|
s.scope, s.itemScope, s.itemPrefix = parentScope, parentItemScope, parentPrefix
|
|
if len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
|
for _, contribution := range itemScope.contributions {
|
|
documents = append(documents, contribution.document)
|
|
}
|
|
completed, diagnostics := composeDocuments(documents...)
|
|
if len(diagnostics) != 0 {
|
|
prefix := destination.Index(0)
|
|
if parentItemScope {
|
|
prefix = appendDestination(parentPrefix, prefix)
|
|
}
|
|
return prependDiagnosticPaths(diagnostics, prefix)
|
|
}
|
|
itemScope.destination = Path{}
|
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(itemScope)}
|
|
projectNestedEvidence(completed, itemScope)
|
|
pl := destination
|
|
_, placed, err := placeDocumentWithMode(completed.root, pl, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
|
if err != nil {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &declaration.location)}
|
|
}
|
|
placementNodes := destinationWrapperNodeCountForMode(pl, placementModeSequenceItem)
|
|
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
|
|
}
|
|
s.accounting.nodes += placementNodes
|
|
protectionRule := s.rule
|
|
if s.versionState != nil && s.versionState.effectiveRule != nil {
|
|
protectionRule = s.versionState.effectiveRule
|
|
}
|
|
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
|
|
versionLocation := publicLocation(versionNode.Location)
|
|
return []Diagnostic{{
|
|
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
|
|
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
|
|
}}
|
|
}
|
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
|
source: cloneSourceInfo(info), payload: completed.root, document: placed,
|
|
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
|
nested: completed,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic {
|
|
cloned := append([]Diagnostic(nil), diagnostics...)
|
|
for index := range cloned {
|
|
cloned[index].Path = appendDestination(prefix, cloned[index].Path)
|
|
cloned[index].Primary = cloneLocation(cloned[index].Primary)
|
|
cloned[index].Related = cloneLocations(cloned[index].Related)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func projectNestedEvidence(composed *composedDocument, scope acquisitionScope) {
|
|
if composed == nil {
|
|
return
|
|
}
|
|
for sourceIndex, contribution := range scope.contributions {
|
|
if contribution.nested == nil {
|
|
continue
|
|
}
|
|
start := 0
|
|
for _, assigned := range composed.sequenceRanges {
|
|
if assigned.sourceIndex == sourceIndex && assigned.path.String() == contribution.destination.String() {
|
|
start = assigned.start
|
|
break
|
|
}
|
|
}
|
|
prefix := contribution.destination.Index(start)
|
|
if sourceIndex < len(composed.applications) {
|
|
outer := composed.applications[sourceIndex]
|
|
structural := document.Clone(outer.value)
|
|
if node, found := documentAtPath(structural, contribution.destination); found && node.Kind == document.Sequence && len(node.Sequence) > start {
|
|
node.Sequence[start] = &document.Node{Kind: document.Mapping, Location: node.Sequence[start].Location}
|
|
}
|
|
outer.value = structural
|
|
composed.applications[sourceIndex] = outer
|
|
for _, nested := range contribution.nested.applications {
|
|
projected := nested
|
|
projected.path = appendDestination(prefix, nested.path)
|
|
projected.sequences = cloneSequenceHistoryContributions(nested.sequences)
|
|
for index := range projected.sequences {
|
|
projected.sequences[index].path = appendDestination(prefix, projected.sequences[index].path)
|
|
}
|
|
composed.applications = append(composed.applications, projected)
|
|
}
|
|
}
|
|
projectNestedOrigins(composed.origins, contribution.nested.root, contribution.nested.origins, prefix)
|
|
}
|
|
}
|
|
|
|
func projectNestedOrigins(target map[string]Origin, root *document.Node, origins map[string]Origin, prefix Path) {
|
|
var visit func(*document.Node, Path)
|
|
visit = func(node *document.Node, path Path) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
if origin, ok := origins[path.String()]; ok {
|
|
target[appendDestination(prefix, path).String()] = cloneOrigin(origin)
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
visit(entry.Value, path.Key(entry.Key))
|
|
}
|
|
for index, child := range node.Sequence {
|
|
visit(child, path.Index(index))
|
|
}
|
|
}
|
|
visit(root, Path{})
|
|
}
|
|
|
|
func projectAcquiredDestinations(composed *composedDocument, scope acquisitionScope, sources []SourceInfo) {
|
|
byID := make(map[string]int, len(sources))
|
|
for index := range sources {
|
|
byID[sources[index].ID] = index
|
|
}
|
|
var projectScope func(*composedDocument, acquisitionScope, Path, bool, string)
|
|
projectScope = func(currentComposition *composedDocument, current acquisitionScope, prefix Path, insideItem bool, parentSourceID string) {
|
|
for sourceIndex, contribution := range current.contributions {
|
|
if contribution.source.ID == parentSourceID {
|
|
continue
|
|
}
|
|
contributionPrefix := prefix
|
|
contributionInsideItem := insideItem
|
|
start := 0
|
|
for _, assigned := range currentComposition.sequenceRanges {
|
|
if assigned.sourceIndex == sourceIndex && assigned.path.String() == contribution.destination.String() {
|
|
start = assigned.start
|
|
break
|
|
}
|
|
}
|
|
if contribution.source.placementMode == placementModeSequenceItem {
|
|
container := appendDestination(contributionPrefix, contribution.destination)
|
|
contribution.source.Destination = container.clone()
|
|
contribution.source.destinationUnresolved = false
|
|
current.contributions[sourceIndex].source = cloneSourceInfo(contribution.source)
|
|
if position, ok := byID[contribution.source.ID]; ok {
|
|
sources[position].Destination = container
|
|
sources[position].destinationUnresolved = false
|
|
}
|
|
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
|
contributionInsideItem = true
|
|
} else if contributionInsideItem {
|
|
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
|
contribution.source.Destination = exactDestination.clone()
|
|
contribution.source.destinationUnresolved = false
|
|
current.contributions[sourceIndex].source = cloneSourceInfo(contribution.source)
|
|
if position, ok := byID[contribution.source.ID]; ok {
|
|
sources[position].Destination = exactDestination
|
|
sources[position].destinationUnresolved = false
|
|
}
|
|
}
|
|
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
|
for _, nestedScope := range contribution.nested.acquisition {
|
|
projectScope(contribution.nested, nestedScope, contributionPrefix, contributionInsideItem, contribution.source.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
projectScope(composed, scope, Path{}, false, "")
|
|
}
|
|
|
|
func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) {
|
|
remaining := s.limits.bytes - s.accounting.bytes
|
|
limit := min(int64(maxSourceBytes), remaining)
|
|
if limit < 0 {
|
|
limit = 0
|
|
}
|
|
data, failure := readRegularFile(name, s.access.stat, s.access.open, limit)
|
|
if failure != nil {
|
|
if optional && failure.missingTarget {
|
|
return nil, true, nil
|
|
}
|
|
value := sourceReadDiagnostic(failure, sourceID, CodeImportLimitExceeded, "configuration exceeds a source or aggregate byte limit")
|
|
return nil, false, &value
|
|
}
|
|
s.accounting.bytes += int64(len(data))
|
|
return data, false, nil
|
|
}
|
|
|
|
func (s *importTraversal) currentRoute(location Location) importRoute {
|
|
locations := make([]Location, 0, len(s.stack)+1)
|
|
for _, route := range s.stack {
|
|
if len(route.locations) != 0 {
|
|
locations = append(locations, route.locations[len(route.locations)-1])
|
|
}
|
|
}
|
|
locations = append(locations, location)
|
|
return importRoute{locations: locations}
|
|
}
|
|
|
|
func sourceInfos(source *SourceInfo) []SourceInfo {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return []SourceInfo{cloneSourceInfo(*source)}
|
|
}
|
|
|
|
func importDiagnostic(code Code, message string, location *Location) Diagnostic {
|
|
return Diagnostic{Code: code, Severity: SeverityError, Message: message, Primary: cloneLocation(location)}
|
|
}
|
|
|
|
func importCycleDiagnostic(target string, location Location, cycle []importRoute) Diagnostic {
|
|
paths := make([]string, 0, len(cycle)+1)
|
|
related := make([]Location, 0, len(cycle))
|
|
for _, route := range cycle {
|
|
paths = append(paths, route.canonical)
|
|
if len(route.locations) != 0 {
|
|
related = append(related, route.locations[len(route.locations)-1])
|
|
}
|
|
}
|
|
paths = append(paths, target)
|
|
return Diagnostic{
|
|
Code: CodeImportCycle, Severity: SeverityError,
|
|
Message: "import cycle detected: " + strings.Join(paths, " -> "),
|
|
Primary: &location, Related: related,
|
|
}
|
|
}
|
|
|
|
func importRepeatedDiagnostic(location Location, previous, current importRoute) Diagnostic {
|
|
related := append([]Location(nil), previous.locations...)
|
|
if len(current.locations) > 1 {
|
|
related = append(related, current.locations[:len(current.locations)-1]...)
|
|
}
|
|
return Diagnostic{
|
|
Code: CodeImportRepeated, Severity: SeverityError,
|
|
Message: "configuration source is included by more than one import route",
|
|
Primary: &location, Related: related,
|
|
}
|
|
}
|