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, policies ...sequencePolicy) ([]SourceInfo, []Diagnostic) { var policy sequencePolicy if len(policies) != 0 { policy = policies[0] } 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} } if len(policies) == 0 { return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState) } return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState, policy) } 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} } if len(policies) == 0 { return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState) } return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState, policy) } 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, policies ...sequencePolicy) ([]SourceInfo, []Diagnostic) { var policy sequencePolicy if len(policies) != 0 { policy = policies[0] } 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(policies) != 0); len(diagnostics) != 0 { return sources, diagnostics } var processed sourceVersionProcessingResult var failures []Diagnostic if len(policies) != 0 { 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 } } else { if versionState != nil && versionState.mode != sourceVersionModeDefault { processed, failures = versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping) } else { processed = sourceVersionProcessingResult{payload: overlayRoot} } } 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)} } var overlayDiagnostics []Diagnostic if len(policies) != 0 { overlayDiagnostics = base.applyOverlayWithCollectionPolicy(overlayPayload, nil, nil, LayerOverlay, policy, processed.sequences) } else { overlayDiagnostics = base.applyOverlay(overlayPayload, nil, nil, LayerOverlay) } if len(overlayDiagnostics) != 0 { return sources, overlayDiagnostics } 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 }