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-09-23 18:27:29 +00:00
func loadSelectedOverlayDocument ( source Source , context * FilesystemContext , main parsedSource , base * composedDocument , sources [ ] SourceInfo , name string , rule * VersionRule , limits importLimits ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-09-29 17:13:14 +00:00
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 ) {
2026-10-15 15:44:04 +00:00
return loadSelectedOverlayIntoWithVersion ( source , context , main , base , sources , name , rule , limits , nil )
}
2026-10-23 19:30:48 +00:00
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 ]
}
2026-09-29 17:36:20 +00:00
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 }
}
2026-10-23 19:30:48 +00:00
if len ( policies ) == 0 {
return loadSelectedOverlayWithAccessAndVersion ( access , base , sources , name , rule , limits , versionState )
}
return loadSelectedOverlayWithAccessAndVersion ( access , base , sources , name , rule , limits , versionState , policy )
2026-09-29 17:29:01 +00:00
}
2026-09-12 15:58:06 +00:00
rootPath , _ , _ , diagnostic := importFilesystem ( source , context , main )
if diagnostic != nil {
2026-09-29 17:13:14 +00:00
return cloneSourceInfos ( sources ) , [ ] Diagnostic { * diagnostic }
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:29:01 +00:00
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 }
}
2026-10-23 19:30:48 +00:00
if len ( policies ) == 0 {
return loadSelectedOverlayWithAccessAndVersion ( access , base , sources , name , rule , limits , versionState )
}
return loadSelectedOverlayWithAccessAndVersion ( access , base , sources , name , rule , limits , versionState , policy )
2026-09-29 17:29:01 +00:00
}
func loadSelectedOverlayWithAccess ( access * filesystemAccess , base * assemblyAccumulator , sources [ ] SourceInfo , name string , rule * VersionRule , limits importLimits ) ( [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-15 15:44:04 +00:00
return loadSelectedOverlayWithAccessAndVersion ( access , base , sources , name , rule , limits , nil )
}
2026-10-23 19:30:48 +00:00
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 ]
}
2026-09-29 17:29:01 +00:00
closed := false
defer func ( ) {
if ! closed {
_ = access . close ( )
}
} ( )
overlayPath := access . join ( access . rootPath , "overlays/" + name + ".yml" )
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
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 }
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-23 19:30:48 +00:00
if diagnostics := overlayDirectiveDiagnostics ( overlayRoot , Path { } , len ( policies ) != 0 ) ; 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
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 }
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-10-24 01:11:28 +00:00
if versionNode := fragmentVersionClaim ( overlayPayload , rule ) ; len ( policies ) == 0 && versionNode != nil {
2026-09-12 15:58:06 +00:00
location := publicLocation ( versionNode . Location )
2026-09-29 17:13:14 +00:00
return sources , [ ] Diagnostic { {
2026-09-23 18:27:29 +00:00
Code : CodeOverlayVersion , Severity : SeverityError , Path : rule . Path . clone ( ) ,
Message : "overlays cannot contribute to or replace the schema-version path" , Primary : & location ,
2026-09-12 15:58:06 +00:00
} }
}
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-23 19:30:48 +00:00
var overlayDiagnostics [ ] Diagnostic
if len ( policies ) != 0 {
2026-10-23 19:56:06 +00:00
overlayDiagnostics = base . applyOverlayWithCollectionPolicy ( overlayPayload , nil , collectionPolicyScope { } , nil , LayerOverlay , policy , processed . sequences )
2026-10-23 19:30:48 +00:00
} else {
overlayDiagnostics = base . applyOverlay ( overlayPayload , nil , nil , LayerOverlay )
}
if len ( overlayDiagnostics ) != 0 {
return sources , overlayDiagnostics
2026-09-12 15:58:06 +00:00
}
2026-10-24 01:11:28 +00:00
if len ( policies ) != 0 {
// 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-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
}