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 }