178 lines
7.6 KiB
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)
|
|
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
|
|
}
|