Files
ConfigKit/internal/engine/selected_overlay.go

178 lines
7.6 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
}
func loadSelectedOverlayDocument(source Source, context *FilesystemContext, main parsedSource, base *composedDocument, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
assembly := newAssemblyAccumulator(cloneComposedDocument(base))
allSources, diagnostics := loadSelectedOverlayInto(source, context, main, assembly, sources, name, rule, limits)
if len(diagnostics) != 0 {
return nil, allSources, diagnostics
}
return assembly.current, allSources, nil
}
func loadSelectedOverlayInto(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
return loadSelectedOverlayIntoWithVersion(source, context, main, base, sources, name, rule, limits, nil)
}
func loadSelectedOverlayIntoWithVersion(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
if nativeContext(source, context) {
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
if diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
if diagnostic := validateNativeDirectories(access, basePath, mainPath); diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
}
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
if diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
access, err := openHostAccess(rootPath)
if err != nil {
value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected overlay", Location{})
value.Primary = nil
return cloneSourceInfos(sources), []Diagnostic{value}
}
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
}
func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, nil)
}
func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
closed := false
defer func() {
if !closed {
_ = access.close()
}
}()
overlayPath := access.join(access.rootPath, "overlays/"+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/" + 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{}); len(diagnostics) != 0 {
return sources, diagnostics
}
overlayPayload := overlayRoot
if versionState != nil && versionState.mode != sourceVersionModeDefault {
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping)
if len(failures) != 0 {
return sources, failures
}
overlayPayload = processed.payload
}
if versionNode := fragmentVersionClaim(overlayPayload, rule); versionNode != nil {
location := publicLocation(versionNode.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,
}}
}
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)}
}
if diagnostics := base.applyOverlay(overlayPayload, nil, nil, LayerOverlay); len(diagnostics) != 0 {
return sources, diagnostics
}
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
}