2026-09-12 15:58:06 +00:00
package engine
import (
"bytes"
"errors"
"io/fs"
"strconv"
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
)
2026-09-23 18:27:29 +00:00
func activeOverlay ( name string ) bool {
2026-09-12 15:58:06 +00:00
return name != "" && name != "default"
}
2026-09-23 18:27:29 +00:00
func validOverlayName ( name string ) bool {
if ! activeOverlay ( name ) {
2026-09-12 15:58:06 +00:00
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
}
2026-10-25 13:18:54 +00:00
type selectedOverlayRequest struct {
name string
protectionRule * VersionRule
sequencePolicy sequencePolicy
limits importLimits
versionState * sourceVersionState
2026-10-15 15:44:04 +00:00
}
2026-10-25 13:18:54 +00:00
func resolveOrdinaryOverlayFilesystem ( source Source , context * FilesystemContext , main parsedSource , existing * resolvedFilesystemContext ) ( resolvedFilesystemContext , * Diagnostic ) {
2026-10-24 15:59:39 +00:00
if source . kind == sourceNone {
if context == nil {
value := optionDiagnostic ( CodeOptionsInvalid , "NoSource overlays require explicit filesystem context" )
2026-10-25 13:18:54 +00:00
return resolvedFilesystemContext { } , & value
2026-10-24 15:59:39 +00:00
}
2026-09-12 15:58:06 +00:00
}
2026-10-25 13:18:54 +00:00
if existing != nil {
return * existing , nil
}
return resolveFilesystemContext ( source , context , main , nil )
}
func openSelectedOverlayAccess ( resolved resolvedFilesystemContext ) ( * filesystemAccess , * Diagnostic ) {
access , err := openResolvedFilesystemAccess ( resolved )
2026-09-29 17:29:01 +00:00
if err != nil {
value := globFilesystemDiagnostic ( err , "configuration root could not be opened for the selected overlay" , Location { } )
value . Primary = nil
2026-10-25 13:18:54 +00:00
return nil , & value
2026-10-23 19:30:48 +00:00
}
2026-10-25 13:18:54 +00:00
return access , nil
2026-10-15 15:44:04 +00:00
}
2026-10-25 13:18:54 +00:00
func loadSelectedOverlay ( request selectedOverlayRequest , access * filesystemAccess , base * assemblyAccumulator , sources [ ] SourceInfo ) ( [ ] SourceInfo , [ ] Diagnostic ) {
policy := request . sequencePolicy
rule := request . protectionRule
limits := request . limits
versionState := request . versionState
2026-09-29 17:29:01 +00:00
closed := false
defer func ( ) {
if ! closed {
_ = access . close ( )
}
} ( )
2026-10-25 13:18:54 +00:00
overlayPath := access . join ( access . rootPath , "overlays/" + request . name + ".yml" )
2026-09-29 17:29:01 +00:00
canonical , err := access . canonical ( overlayPath )
2026-09-12 15:58:06 +00:00
if err != nil {
2026-09-23 18:27:29 +00:00
code := CodeOverlayInvalid
message := "selected overlay could not be resolved"
2026-09-12 15:58:06 +00:00
if errors . Is ( err , fs . ErrNotExist ) {
2026-09-23 18:27:29 +00:00
code , message = CodeOverlayMissing , "selected overlay does not exist"
2026-09-12 15:58:06 +00:00
} else if errors . Is ( err , fs . ErrPermission ) {
2026-09-23 18:27:29 +00:00
code , message = CodeImportPermission , "selected overlay cannot be accessed"
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( code , message , nil ) }
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:29:01 +00:00
if ! access . contains ( canonical ) {
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( CodeOverlayOutsideRoot , "selected overlay resolves outside the configuration root" , nil ) }
2026-09-12 15:58:06 +00:00
}
for _ , existing := range sources {
2026-09-29 17:58:15 +00:00
if existing . Path != "" && existing . Path == canonical &&
( ( access . native == nil && existing . Kind == SourceKindFile ) ||
( access . native != nil && existing . Kind == SourceKindFilesystem && existing . namespace == access . token ) ) {
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( CodeOverlayRepeated , "selected overlay is already present in the base source graph" , nil ) }
2026-09-12 15:58:06 +00:00
}
}
if len ( sources ) + 1 > limits . sources {
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( CodeOverlayLimitExceeded , "selected overlay exceeds the source-count limit" , nil ) }
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:29:01 +00:00
if _ , err := access . relative ( canonical ) ; err != nil {
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( CodeOverlayOutsideRoot , "selected overlay could not be addressed inside the configuration root" , nil ) }
2026-09-12 15:58:06 +00:00
}
sourceID := "source-" + strconv . Itoa ( len ( sources ) + 1 )
2026-09-23 18:27:29 +00:00
overlayLocation := & Location { SourceID : sourceID }
2026-09-29 17:36:20 +00:00
kind := SourceKindFile
2026-10-25 13:18:54 +00:00
label := "overlays/" + request . name + ".yml"
2026-09-29 17:36:20 +00:00
if access . native != nil {
kind = SourceKindFilesystem
label = access . label + "/" + canonical
}
overlaySource := SourceInfo { ID : sourceID , Label : label , Path : canonical , Layer : LayerOverlay , Kind : kind }
2026-09-29 17:58:15 +00:00
if kind == SourceKindFilesystem {
overlaySource . namespace = access . token
}
2026-09-23 18:27:29 +00:00
sources = append ( cloneSourceInfos ( sources ) , overlaySource )
2026-09-29 17:29:01 +00:00
data , diagnostic := readOverlayFile ( access , canonical , sourceID , limits . bytes - base . current . inputBytes )
2026-09-12 15:58:06 +00:00
if diagnostic != nil {
2026-09-29 17:13:14 +00:00
return sources , [ ] Diagnostic { * diagnostic }
2026-09-12 15:58:06 +00:00
}
2026-09-23 18:27:29 +00:00
overlayRoot , issues := yamladapter . Parse ( bytes . NewReader ( data ) , sourceID , yamladapter . Limits { Depth : maxDocumentDepth , Nodes : maxDocumentNodes } )
2026-09-12 15:58:06 +00:00
if len ( issues ) != 0 {
2026-09-29 17:13:14 +00:00
return sources , parserDiagnostics ( sourceID , issues )
2026-09-12 15:58:06 +00:00
}
2026-09-23 18:27:29 +00:00
nodeCount := countInputNodes ( overlayRoot )
2026-09-29 17:13:14 +00:00
if base . current . inputNodes + nodeCount > limits . nodes {
return sources , [ ] Diagnostic { importDiagnostic ( CodeOverlayLimitExceeded , "selected overlay exceeds the aggregate node limit" , overlayLocation ) }
2026-09-12 15:58:06 +00:00
}
2026-10-25 13:18:54 +00:00
if diagnostics := overlayDirectiveDiagnostics ( overlayRoot , Path { } , true ) ; len ( diagnostics ) != 0 {
2026-09-29 17:13:14 +00:00
return sources , diagnostics
2026-09-12 15:58:06 +00:00
}
2026-10-23 19:30:48 +00:00
var processed sourceVersionProcessingResult
var failures [ ] Diagnostic
2026-10-25 13:18:54 +00:00
if versionState != nil {
processed , failures = versionState . processWithDirectiveContext (
2026-10-26 00:18:12 +00:00
overlayRoot , sourceVersionContribution , Path { } , sourceID , PlacementModeMapping , rootDirectiveContextOverlay ,
2026-10-25 13:18:54 +00:00
)
2026-10-23 19:30:48 +00:00
} else {
2026-10-25 13:18:54 +00:00
processed = processSourceVersionWithDirectiveContext (
overlayRoot , rule , sourceVersionContribution , Path { } , sourceVersionModeDefault , rootDirectiveContextOverlay ,
)
failures = processed . diagnostics
2026-10-15 15:44:04 +00:00
}
2026-10-23 19:30:48 +00:00
if len ( failures ) != 0 {
return sources , failures
}
overlayPayload := processed . payload
2026-09-29 17:29:01 +00:00
closeErr := access . close ( )
closed = true
2026-09-12 15:58:06 +00:00
if closeErr != nil {
2026-09-29 17:13:14 +00:00
return sources , [ ] Diagnostic { importDiagnostic ( CodeImportRead , "configuration root could not be closed after reading the selected overlay" , nil ) }
2026-09-12 15:58:06 +00:00
}
2026-10-25 13:18:54 +00:00
overlayDiagnostics := base . applyOverlayWithCollectionPolicy ( overlayPayload , nil , collectionPolicyScope { } , nil , LayerOverlay , policy , processed . sequences )
2026-10-23 19:30:48 +00:00
if len ( overlayDiagnostics ) != 0 {
return sources , overlayDiagnostics
2026-09-12 15:58:06 +00:00
}
2026-10-25 13:18:54 +00:00
// 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 ,
} }
2026-10-24 01:11:28 +00:00
}
2026-09-29 17:13:14 +00:00
base . current . inputBytes += int64 ( len ( data ) )
base . current . inputNodes += nodeCount
return sources , nil
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:29:01 +00:00
func readOverlayFile ( access * filesystemAccess , name , sourceID string , remaining int64 ) ( [ ] byte , * Diagnostic ) {
2026-09-12 15:58:06 +00:00
limit := min ( int64 ( maxSourceBytes ) , remaining )
if limit < 0 {
limit = 0
}
2026-09-29 17:29:01 +00:00
data , failure := readRegularFile ( name , access . stat , access . open , limit )
2026-09-12 15:58:06 +00:00
if failure != nil {
2026-09-23 18:27:29 +00:00
value := sourceReadDiagnostic ( failure , sourceID , CodeOverlayLimitExceeded , "selected overlay exceeds a source or aggregate byte limit" )
2026-09-12 15:58:06 +00:00
return nil , & value
}
return data , nil
}