185 lines
6.9 KiB
Go
185 lines
6.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io/fs"
|
|
"strconv"
|
|
|
|
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
|
)
|
|
|
|
func activeOverlay(name string) bool {
|
|
return name != "" && name != "default"
|
|
}
|
|
|
|
func validOverlayName(name string) bool {
|
|
if !activeOverlay(name) {
|
|
return true
|
|
}
|
|
for _, character := range name {
|
|
if (character >= 'a' && character <= 'z') ||
|
|
(character >= 'A' && character <= 'Z') ||
|
|
(character >= '0' && character <= '9') || character == '_' || character == '-' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type selectedOverlayRequest struct {
|
|
name string
|
|
filesystem resolvedFilesystemContext
|
|
protectionRule *VersionRule
|
|
sequencePolicy sequencePolicy
|
|
limits importLimits
|
|
versionState *sourceVersionState
|
|
}
|
|
|
|
func resolveOrdinaryOverlayFilesystem(source Source, context *FilesystemContext, main parsedSource, existing *resolvedFilesystemContext) (resolvedFilesystemContext, *Diagnostic) {
|
|
if source.kind == sourceNone {
|
|
if context == nil {
|
|
value := optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")
|
|
return resolvedFilesystemContext{}, &value
|
|
}
|
|
}
|
|
if existing != nil {
|
|
return *existing, nil
|
|
}
|
|
return resolveFilesystemContext(source, context, main, nil)
|
|
}
|
|
|
|
func openSelectedOverlayAccess(resolved resolvedFilesystemContext) (*filesystemAccess, *Diagnostic) {
|
|
access, err := openResolvedFilesystemAccess(resolved)
|
|
if err != nil {
|
|
value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected overlay", Location{})
|
|
value.Primary = nil
|
|
return nil, &value
|
|
}
|
|
return access, nil
|
|
}
|
|
|
|
func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo) ([]SourceInfo, []Diagnostic) {
|
|
policy := request.sequencePolicy
|
|
rule := request.protectionRule
|
|
limits := request.limits
|
|
versionState := request.versionState
|
|
closed := false
|
|
defer func() {
|
|
if !closed {
|
|
_ = access.close()
|
|
}
|
|
}()
|
|
overlayPath := access.join(access.rootPath, "overlays/"+request.name+".yml")
|
|
canonical, err := access.canonical(overlayPath)
|
|
if err != nil {
|
|
code := CodeOverlayInvalid
|
|
message := "selected overlay could not be resolved"
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
code, message = CodeOverlayMissing, "selected overlay does not exist"
|
|
} else if errors.Is(err, fs.ErrPermission) {
|
|
code, message = CodeImportPermission, "selected overlay cannot be accessed"
|
|
}
|
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(code, message, nil)}
|
|
}
|
|
if !access.contains(canonical) {
|
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)}
|
|
}
|
|
for _, existing := range sources {
|
|
if existing.Path != "" && existing.Path == canonical &&
|
|
((access.native == nil && existing.Kind == SourceKindFile) ||
|
|
(access.native != nil && existing.Kind == SourceKindFilesystem && existing.namespace == access.token)) {
|
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
|
|
}
|
|
}
|
|
if len(sources)+1 > limits.sources {
|
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)}
|
|
}
|
|
|
|
if _, err := access.relative(canonical); err != nil {
|
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay could not be addressed inside the configuration root", nil)}
|
|
}
|
|
|
|
sourceID := "source-" + strconv.Itoa(len(sources)+1)
|
|
overlayLocation := &Location{SourceID: sourceID}
|
|
kind := SourceKindFile
|
|
label := "overlays/" + request.name + ".yml"
|
|
if access.native != nil {
|
|
kind = SourceKindFilesystem
|
|
label = access.label + "/" + canonical
|
|
}
|
|
overlaySource := SourceInfo{ID: sourceID, Label: label, Path: canonical, Layer: LayerOverlay, Kind: kind}
|
|
if kind == SourceKindFilesystem {
|
|
overlaySource.namespace = access.token
|
|
}
|
|
sources = append(cloneSourceInfos(sources), overlaySource)
|
|
data, diagnostic := readOverlayFile(access, canonical, sourceID, limits.bytes-base.current.inputBytes)
|
|
if diagnostic != nil {
|
|
return sources, []Diagnostic{*diagnostic}
|
|
}
|
|
overlayRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes})
|
|
if len(issues) != 0 {
|
|
return sources, parserDiagnostics(sourceID, issues)
|
|
}
|
|
nodeCount := countInputNodes(overlayRoot)
|
|
if base.current.inputNodes+nodeCount > limits.nodes {
|
|
return sources, []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the aggregate node limit", overlayLocation)}
|
|
}
|
|
if diagnostics := overlayDirectiveDiagnostics(overlayRoot, Path{}, true); len(diagnostics) != 0 {
|
|
return sources, diagnostics
|
|
}
|
|
var processed sourceVersionProcessingResult
|
|
var failures []Diagnostic
|
|
if versionState != nil {
|
|
processed, failures = versionState.processWithDirectiveContext(
|
|
overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping, rootDirectiveContextOverlay,
|
|
)
|
|
} else {
|
|
processed = processSourceVersionWithDirectiveContext(
|
|
overlayRoot, rule, sourceVersionContribution, Path{}, sourceVersionModeDefault, rootDirectiveContextOverlay,
|
|
)
|
|
failures = processed.diagnostics
|
|
}
|
|
if len(failures) != 0 {
|
|
return sources, failures
|
|
}
|
|
overlayPayload := processed.payload
|
|
closeErr := access.close()
|
|
closed = true
|
|
if closeErr != nil {
|
|
return sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected overlay", nil)}
|
|
}
|
|
|
|
overlayDiagnostics := base.applyOverlayWithCollectionPolicy(overlayPayload, nil, collectionPolicyScope{}, nil, LayerOverlay, policy, processed.sequences)
|
|
if len(overlayDiagnostics) != 0 {
|
|
return sources, overlayDiagnostics
|
|
}
|
|
// The selected overlay retains one application with the actual item
|
|
// assignments. Raw list indexes cannot identify an upsert's destination.
|
|
application := base.current.applications[len(base.current.applications)-1]
|
|
if node := versionClaimFromApplication(application, rule); node != nil {
|
|
location := publicLocation(node.Location)
|
|
return sources, []Diagnostic{{
|
|
Code: CodeOverlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
|
Message: "overlays cannot contribute to or replace the schema-version path", Primary: &location,
|
|
}}
|
|
}
|
|
base.current.inputBytes += int64(len(data))
|
|
base.current.inputNodes += nodeCount
|
|
return sources, nil
|
|
}
|
|
|
|
func readOverlayFile(access *filesystemAccess, name, sourceID string, remaining int64) ([]byte, *Diagnostic) {
|
|
limit := min(int64(maxSourceBytes), remaining)
|
|
if limit < 0 {
|
|
limit = 0
|
|
}
|
|
data, failure := readRegularFile(name, access.stat, access.open, limit)
|
|
if failure != nil {
|
|
value := sourceReadDiagnostic(failure, sourceID, CodeOverlayLimitExceeded, "selected overlay exceeds a source or aggregate byte limit")
|
|
return nil, &value
|
|
}
|
|
return data, nil
|
|
}
|