2026-09-12 15:58:06 +00:00
package engine
import (
"bytes"
"errors"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
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-12 15:58:06 +00:00
rootPath , _ , _ , diagnostic := importFilesystem ( source , context , main )
if diagnostic != nil {
return nil , cloneSourceInfos ( sources ) , [ ] Diagnostic { * diagnostic }
}
2026-09-23 18:27:29 +00:00
overlayPath := filepath . Join ( rootPath , "overlays" , name + ".yml" )
canonical , err := canonicalExistingPath ( 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
}
return nil , cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( code , message , nil ) }
}
if ! containedPath ( rootPath , canonical ) {
2026-09-23 18:27:29 +00:00
return nil , 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 {
if existing . Path != "" && existing . Path == canonical {
2026-09-23 18:27:29 +00:00
return nil , 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-23 18:27:29 +00:00
return nil , cloneSourceInfos ( sources ) , [ ] Diagnostic { importDiagnostic ( CodeOverlayLimitExceeded , "selected overlay exceeds the source-count limit" , nil ) }
2026-09-12 15:58:06 +00:00
}
root , err := os . OpenRoot ( rootPath )
if err != nil {
2026-09-23 18:27:29 +00:00
value := globFilesystemDiagnostic ( err , "configuration root could not be opened for the selected overlay" , Location { } )
2026-09-12 15:58:06 +00:00
value . Primary = nil
return nil , cloneSourceInfos ( sources ) , [ ] Diagnostic { value }
}
rootClosed := false
defer func ( ) {
if ! rootClosed {
_ = root . Close ( )
}
} ( )
relative , err := filepath . Rel ( rootPath , canonical )
if err != nil || filepath . IsAbs ( relative ) || relative == ".." || strings . HasPrefix ( relative , ".." + string ( filepath . Separator ) ) {
2026-09-23 18:27:29 +00:00
return nil , 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 }
overlaySource := SourceInfo { ID : sourceID , Label : filepath . ToSlash ( filepath . Join ( "overlays" , name + ".yml" ) ) , Path : canonical , Layer : LayerOverlay }
sources = append ( cloneSourceInfos ( sources ) , overlaySource )
data , diagnostic := readOverlayFile ( root , relative , sourceID , limits . bytes - base . inputBytes )
2026-09-12 15:58:06 +00:00
if diagnostic != nil {
return nil , sources , [ ] Diagnostic { * diagnostic }
}
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 {
return nil , sources , parserDiagnostics ( sourceID , issues )
}
2026-09-23 18:27:29 +00:00
nodeCount := countInputNodes ( overlayRoot )
2026-09-12 15:58:06 +00:00
if base . inputNodes + nodeCount > limits . nodes {
2026-09-23 18:27:29 +00:00
return nil , sources , [ ] Diagnostic { importDiagnostic ( CodeOverlayLimitExceeded , "selected overlay exceeds the aggregate node limit" , overlayLocation ) }
2026-09-12 15:58:06 +00:00
}
2026-09-23 18:27:29 +00:00
if diagnostics := overlayDirectiveDiagnostics ( overlayRoot , Path { } ) ; len ( diagnostics ) != 0 {
2026-09-12 15:58:06 +00:00
return nil , sources , diagnostics
}
2026-09-23 18:27:29 +00:00
if versionNode := fragmentVersionClaim ( overlayRoot , rule ) ; versionNode != nil {
2026-09-12 15:58:06 +00:00
location := publicLocation ( versionNode . Location )
return nil , 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
} }
}
closeErr := root . Close ( )
rootClosed = true
if closeErr != nil {
2026-09-23 18:27:29 +00:00
return nil , 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-09-23 18:27:29 +00:00
overlayd , diagnostics := overlayDocuments ( base , overlayRoot )
if overlayd != nil {
overlayd . inputBytes = base . inputBytes + int64 ( len ( data ) )
overlayd . inputNodes = base . inputNodes + nodeCount
2026-09-12 15:58:06 +00:00
}
2026-09-23 18:27:29 +00:00
return overlayd , sources , diagnostics
2026-09-12 15:58:06 +00:00
}
2026-09-23 18:27:29 +00:00
func readOverlayFile ( root * os . Root , relative , sourceID string , remaining int64 ) ( [ ] byte , * Diagnostic ) {
2026-09-12 15:58:06 +00:00
limit := min ( int64 ( maxSourceBytes ) , remaining )
if limit < 0 {
limit = 0
}
data , failure := readRegularFile ( relative , root . Stat , root . Open , limit )
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
}