package engine import ( "errors" "io/fs" "path" "slices" "sort" "strings" ) const globReadBatch = 128 // 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) // nextByPath is the set union of candidates reached from each matching // parent at this segment; final exclusions below are set difference. nextByPath := make(map[string]selectedImportCandidate) for _, parent := range candidates { if !patterned { childPath := access.join(parent.path, literal) childAccessPath := access.join(parent.accessPath, literal) candidate, missing, diagnostics := explicitGlobCandidate(access, basePath, childPath, childAccessPath, site, last) 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()) lexicalPath := access.join(parent.path, entry.Name()) if !access.contains(accessPath) || !access.contains(lexicalPath) { 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 } physicalPath := target.identity // Native source identity and relative imports use the namespace // spelling; resolved link identities belong only to discovery. if access.native != nil { physicalPath = accessPath } 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, } 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 } func explicitGlobCandidate(access *filesystemAccess, basePath, candidatePath, accessPath string, site importFieldSite, last bool) (selectedImportCandidate, bool, []Diagnostic) { if !access.contains(candidatePath) || !access.contains(accessPath) { return selectedImportCandidate{}, false, []Diagnostic{*importSelectionDiagnostic(site, CodeImportOutsideRoot, "glob traversal is outside the configuration root")} } target, err := access.importEntryTarget(accessPath, nil, true) if err != nil { if errors.Is(err, fs.ErrNotExist) { return selectedImportCandidate{}, true, nil } return selectedImportCandidate{}, false, []Diagnostic{globCandidateDiagnostic(err, site.location, "glob match could not be inspected")} } canonical, err := access.canonical(accessPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return selectedImportCandidate{}, true, nil } return selectedImportCandidate{}, false, []Diagnostic{globFilesystemDiagnostic(err, "glob match could not be canonicalized", site.location)} } if !access.contains(canonical) { return selectedImportCandidate{}, false, []Diagnostic{*importSelectionDiagnostic(site, CodeImportOutsideRoot, "glob traversal resolves outside the configuration root")} } if !last { if !target.dir { return selectedImportCandidate{}, true, nil } } return selectedImportCandidate{ path: normalizedImportCandidatePath(access, candidatePath), accessPath: canonical, label: relativeImportLabel(access, basePath, candidatePath), site: site, }, false, nil } func readGlobEntries(access *filesystemAccess, directory string, location Location, limits importLimits, accounting *importAccounting) ([]fs.DirEntry, bool, []Diagnostic) { target, err := access.importEntryTarget(directory, nil, true) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, true, nil } 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 } 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)} } readPath := target.identity if readPath == "" { readPath = canonical } entries := make([]fs.DirEntry, 0) err = visitBoundedImportDirectory(access, readPath, limits.entries, accounting, func(entry fs.DirEntry) error { entries = append(entries, entry) return nil }) if errors.Is(err, errImportDirectoryEntryLimit) { return nil, false, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)} } if errors.Is(err, fs.ErrNotExist) { return nil, true, nil } if err != nil { 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 } 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) }