package engine import ( "errors" "io/fs" "path" "slices" "sort" "strings" ) const globReadBatch = 128 type globCandidate struct { absolute string relative string } func (s *importTraversal) expandImportGlob(basePath string, declaration importDeclaration) ([]string, []Diagnostic) { location := declaration.location segments, valid := validGlobPattern(declaration.glob) if !valid { return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "import glob pattern is invalid or uses recursive ** syntax", &location)} } candidates := []globCandidate{{absolute: basePath}} for index, segment := range segments { last := index == len(segments)-1 literal, patterned := globLiteralSegment(segment) var next []globCandidate for _, candidate := range candidates { var names []string if patterned { matched, diagnostics := s.readGlobMatches(candidate.absolute, segment, location) if len(diagnostics) != 0 { return nil, diagnostics } names = matched } else { names = []string{literal} } for _, name := range names { absolute := s.access.join(candidate.absolute, name) relative := path.Join(candidate.relative, name) if !s.access.contains(absolute) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal is outside the configuration root", &location)} } canonical, err := s.access.canonical(absolute) if err != nil { if errors.Is(err, fs.ErrNotExist) && !patterned { continue } return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob match could not be canonicalized", location)} } if !s.access.contains(canonical) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal resolves outside the configuration root", &location)} } if last { next = append(next, globCandidate{absolute: canonical, relative: relative}) continue } info, err := s.access.stat(canonical) if err != nil { return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob traversal target could not be inspected", location)} } if info.IsDir() { next = append(next, globCandidate{absolute: canonical, relative: relative}) } } } candidates = next if len(candidates) == 0 { return nil, nil } } matches := make([]string, len(candidates)) for index, candidate := range candidates { matches[index] = candidate.relative } slices.Sort(matches) return matches, nil } // 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 := make(map[string]selectedImportCandidate) for _, parent := range candidates { if !patterned { childPath := access.join(parent.accessPath, literal) candidate, missing, diagnostics := explicitGlobCandidate(access, childPath, 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()) if !access.contains(accessPath) { return importSelection{}, []Diagnostic{*importSelectionDiagnostic(site, CodeImportOutsideRoot, "glob traversal is outside the configuration root")} } candidate := selectedImportCandidate{path: normalizedImportCandidatePath(access, accessPath), accessPath: accessPath, site: site} 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 } 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, candidatePath string, site importFieldSite, last bool) (selectedImportCandidate, bool, []Diagnostic) { if !access.contains(candidatePath) { return selectedImportCandidate{}, false, []Diagnostic{*importSelectionDiagnostic(site, CodeImportOutsideRoot, "glob traversal is outside the configuration root")} } target, err := access.importEntryTarget(candidatePath, 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(candidatePath) 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: candidatePath, site: site, }, false, nil } func readGlobEntries(access *filesystemAccess, directory string, location Location, limits importLimits, accounting *importAccounting) ([]fs.DirEntry, bool, []Diagnostic) { if _, err := access.importEntryTarget(directory, nil, true); 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)} } entries := make([]fs.DirEntry, 0) err = visitBoundedImportDirectory(access, directory, 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 (s *importTraversal) readGlobMatches(directory, pattern string, location Location) ([]string, []Diagnostic) { entries, missing, diagnostics := readGlobEntries(s.access, directory, location, s.limits, s.accounting) if len(diagnostics) != 0 { return nil, diagnostics } if missing { return nil, nil } var matches []string for _, entry := range entries { matched, matchErr := path.Match(pattern, entry.Name()) if matchErr != nil { return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "import glob pattern is invalid", &location)} } if matched { matches = append(matches, entry.Name()) } } return matches, nil } 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) }