2026-09-12 15:58:06 +00:00
package engine
import (
"errors"
"io/fs"
"path"
"slices"
2026-10-19 20:38:42 +00:00
"sort"
2026-09-12 15:58:06 +00:00
"strings"
)
const globReadBatch = 128
2026-10-19 20:38:42 +00:00
// selectGlobImportCandidates expands path components while retaining the
// lexical spelling used for filtering and later source admission.
func selectGlobImportCandidates ( access * filesystemAccess , basePath string , spec importDeclarationSpec , policy resolvedImportPolicy , limits importLimits , accounting * importAccounting ) ( importSelection , [ ] Diagnostic ) {
site := spec . fieldSites [ "glob" ]
segments := spec . globSegments
if len ( segments ) == 0 {
var valid bool
segments , valid = validGlobPattern ( spec . glob )
if ! valid {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportInvalid , "import glob pattern is invalid" ) }
}
}
if accounting == nil {
accounting = & importAccounting { }
}
rootPath := normalizedImportCandidatePath ( access , basePath )
candidates := [ ] selectedImportCandidate { { path : rootPath , accessPath : basePath , site : site } }
for index , segment := range segments {
last := index == len ( segments ) - 1
literal , patterned := globLiteralSegment ( segment )
2026-10-19 21:23:47 +00:00
// nextByPath is the set union of candidates reached from each matching
// parent at this segment; final exclusions below are set difference.
2026-10-19 20:38:42 +00:00
nextByPath := make ( map [ string ] selectedImportCandidate )
for _ , parent := range candidates {
if ! patterned {
2026-10-19 20:50:47 +00:00
childPath := access . join ( parent . path , literal )
childAccessPath := access . join ( parent . accessPath , literal )
candidate , missing , diagnostics := explicitGlobCandidate ( access , basePath , childPath , childAccessPath , site , last )
2026-10-19 20:38:42 +00:00
if len ( diagnostics ) != 0 {
return importSelection { } , diagnostics
}
if ! missing {
nextByPath [ candidate . path ] = candidate
}
continue
}
entries , missing , diagnostics := readGlobEntries ( access , parent . accessPath , site . location , limits , accounting )
if len ( diagnostics ) != 0 {
return importSelection { } , diagnostics
}
if missing {
continue
}
for _ , entry := range entries {
matched , err := path . Match ( segment , entry . Name ( ) )
if err != nil {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportInvalid , "import glob pattern is invalid" ) }
}
if ! matched || ! policy . hidden && strings . HasPrefix ( entry . Name ( ) , "." ) {
continue
}
accessPath := access . join ( parent . accessPath , entry . Name ( ) )
2026-10-19 20:50:47 +00:00
lexicalPath := access . join ( parent . path , entry . Name ( ) )
if ! access . contains ( accessPath ) || ! access . contains ( lexicalPath ) {
2026-10-19 20:38:42 +00:00
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportOutsideRoot , "glob traversal is outside the configuration root" ) }
}
target , err := access . importEntryTarget ( accessPath , entry , policy . follow )
if err != nil {
return importSelection { } , [ ] Diagnostic { globCandidateDiagnostic ( err , site . location , "glob match could not be inspected" ) }
}
if target . symlink && ! policy . follow {
continue
}
if ! last && ! target . dir {
continue
}
2026-10-19 20:50:47 +00:00
physicalPath := target . identity
2026-10-20 00:20:38 +00:00
// Native source identity and relative imports use the namespace
// spelling; resolved link identities belong only to discovery.
if access . native != nil {
physicalPath = accessPath
}
2026-10-19 20:50:47 +00:00
if physicalPath == "" {
physicalPath , err = access . canonical ( accessPath )
if err != nil {
return importSelection { } , [ ] Diagnostic { globFilesystemDiagnostic ( err , "glob match could not be canonicalized" , site . location ) }
}
}
candidate := selectedImportCandidate {
path : normalizedImportCandidatePath ( access , lexicalPath ) , accessPath : physicalPath ,
label : relativeImportLabel ( access , basePath , lexicalPath ) , site : site ,
}
2026-10-19 20:38:42 +00:00
nextByPath [ candidate . path ] = candidate
}
}
candidates = candidatesFromMap ( nextByPath )
if len ( candidates ) == 0 {
break
}
}
selected := make ( [ ] selectedImportCandidate , 0 , len ( candidates ) )
for _ , candidate := range candidates {
missing , _ , diagnostic := validateNamedImportTarget ( access , candidate , false , false )
if diagnostic != nil {
return importSelection { } , [ ] Diagnostic { * diagnostic }
}
if missing || excludedBasename ( path . Base ( candidate . path ) , policy . exclude ) {
continue
}
selected = append ( selected , candidate )
}
if policy . required && len ( selected ) == 0 {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportMissing , "required import glob selected no files" ) }
}
// Source allowance is charged by source admission, after this filtered set
// reaches the traversal; that preserves repeated-source diagnostics first.
selection := importSelection { candidates : selected }
if len ( selected ) != 0 {
selection . outcome = importSelectionHasCandidates
}
return selection , nil
}
2026-10-19 20:50:47 +00:00
func explicitGlobCandidate ( access * filesystemAccess , basePath , candidatePath , accessPath string , site importFieldSite , last bool ) ( selectedImportCandidate , bool , [ ] Diagnostic ) {
if ! access . contains ( candidatePath ) || ! access . contains ( accessPath ) {
2026-10-19 20:38:42 +00:00
return selectedImportCandidate { } , false , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportOutsideRoot , "glob traversal is outside the configuration root" ) }
}
2026-10-19 20:50:47 +00:00
target , err := access . importEntryTarget ( accessPath , nil , true )
2026-09-12 15:58:06 +00:00
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
2026-10-19 20:38:42 +00:00
return selectedImportCandidate { } , true , nil
}
return selectedImportCandidate { } , false , [ ] Diagnostic { globCandidateDiagnostic ( err , site . location , "glob match could not be inspected" ) }
}
2026-10-19 20:50:47 +00:00
canonical , err := access . canonical ( accessPath )
2026-10-19 20:38:42 +00:00
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
return selectedImportCandidate { } , true , nil
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
return selectedImportCandidate { } , false , [ ] Diagnostic { globFilesystemDiagnostic ( err , "glob match could not be canonicalized" , site . location ) }
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
if ! access . contains ( canonical ) {
return selectedImportCandidate { } , false , [ ] Diagnostic { * importSelectionDiagnostic ( site , CodeImportOutsideRoot , "glob traversal resolves outside the configuration root" ) }
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
if ! last {
if ! target . dir {
return selectedImportCandidate { } , true , nil
}
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
return selectedImportCandidate {
2026-10-19 20:50:47 +00:00
path : normalizedImportCandidatePath ( access , candidatePath ) , accessPath : canonical ,
label : relativeImportLabel ( access , basePath , candidatePath ) , site : site ,
2026-10-19 20:38:42 +00:00
} , false , nil
}
func readGlobEntries ( access * filesystemAccess , directory string , location Location , limits importLimits , accounting * importAccounting ) ( [ ] fs . DirEntry , bool , [ ] Diagnostic ) {
2026-10-19 20:50:47 +00:00
target , err := access . importEntryTarget ( directory , nil , true )
if err != nil {
2026-10-19 20:38:42 +00:00
if errors . Is ( err , fs . ErrNotExist ) {
return nil , true , nil
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
return nil , false , [ ] Diagnostic { globCandidateDiagnostic ( err , location , "glob directory could not be inspected" ) }
}
canonical , err := access . canonical ( directory )
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
return nil , true , nil
2026-09-12 15:58:06 +00:00
}
2026-10-19 20:38:42 +00:00
return nil , false , [ ] Diagnostic { globFilesystemDiagnostic ( err , "glob directory could not be canonicalized" , location ) }
}
if ! access . contains ( canonical ) {
return nil , false , [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "glob directory resolves outside the configuration root" , & location ) }
}
if _ , err := access . relative ( canonical ) ; err != nil {
return nil , false , [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "glob directory could not be addressed inside the configuration root" , & location ) }
}
2026-10-19 20:50:47 +00:00
readPath := target . identity
if readPath == "" {
readPath = canonical
}
2026-10-19 20:38:42 +00:00
entries := make ( [ ] fs . DirEntry , 0 )
2026-10-19 20:50:47 +00:00
err = visitBoundedImportDirectory ( access , readPath , limits . entries , accounting , func ( entry fs . DirEntry ) error {
2026-10-19 20:38:42 +00:00
entries = append ( entries , entry )
2026-10-19 20:18:39 +00:00
return nil
2026-09-29 17:29:01 +00:00
} )
2026-10-19 20:18:39 +00:00
if errors . Is ( err , errImportDirectoryEntryLimit ) {
2026-10-19 20:38:42 +00:00
return nil , false , [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "glob traversal exceeds the directory-entry visit limit" , & location ) }
2026-09-12 15:58:06 +00:00
}
2026-10-25 21:33:36 +00:00
if missingDirectoryOpen ( err ) {
2026-10-19 20:38:42 +00:00
return nil , true , nil
2026-09-29 17:29:01 +00:00
}
if err != nil {
2026-10-19 20:38:42 +00:00
return nil , false , [ ] Diagnostic { globFilesystemDiagnostic ( err , "glob directory could not be traversed" , location ) }
}
sort . Slice ( entries , func ( left , right int ) bool { return entries [ left ] . Name ( ) < entries [ right ] . Name ( ) } )
return entries , false , nil
}
func globCandidateDiagnostic ( err error , location Location , message string ) Diagnostic {
if errors . Is ( err , errImportDirectoryLinkCycle ) {
return importDiagnostic ( CodeImportCycle , "directory traversal cycle detected" , & location )
}
if errors . Is ( err , errImportPathOutsideRoot ) {
return importDiagnostic ( CodeImportOutsideRoot , "glob traversal resolves outside the configuration root" , & location )
}
return globFilesystemDiagnostic ( err , message , location )
}
func candidatesFromMap ( candidates map [ string ] selectedImportCandidate ) [ ] selectedImportCandidate {
paths := make ( [ ] string , 0 , len ( candidates ) )
for candidatePath := range candidates {
paths = append ( paths , candidatePath )
}
slices . Sort ( paths )
result := make ( [ ] selectedImportCandidate , 0 , len ( paths ) )
for _ , candidatePath := range paths {
result = append ( result , candidates [ candidatePath ] )
}
return result
}
2026-09-12 15:58:06 +00:00
func validGlobPattern ( pattern string ) ( [ ] string , bool ) {
if ! validImportPath ( pattern ) {
return nil , false
}
segments := strings . Split ( pattern , "/" )
for _ , segment := range segments {
if segment == "" || hasRecursiveStars ( segment ) {
return nil , false
}
if _ , err := path . Match ( segment , "" ) ; err != nil {
return nil , false
}
}
return segments , true
}
func hasRecursiveStars ( segment string ) bool {
inClass := false
for index := 0 ; index < len ( segment ) ; index ++ {
switch segment [ index ] {
case '\\' :
index ++
case '[' :
inClass = true
case ']' :
inClass = false
case '*' :
if ! inClass && index + 1 < len ( segment ) && segment [ index + 1 ] == '*' {
return true
}
}
}
return false
}
func globLiteralSegment ( segment string ) ( string , bool ) {
var literal strings . Builder
for index := 0 ; index < len ( segment ) ; index ++ {
switch segment [ index ] {
case '*' , '?' , '[' :
return "" , true
case '\\' :
index ++
if index < len ( segment ) {
literal . WriteByte ( segment [ index ] )
}
default :
literal . WriteByte ( segment [ index ] )
}
}
return literal . String ( ) , false
}
func globFilesystemDiagnostic ( err error , message string , location Location ) Diagnostic {
code := CodeImportRead
if errors . Is ( err , fs . ErrPermission ) {
code = CodeImportPermission
}
return importDiagnostic ( code , message , & location )
}