package engine import ( "errors" "io/fs" "path" "slices" "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}) if s.accounting.sources+len(next) > s.limits.sources { return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob matches exceed the remaining source-count limit", &location)} } 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 } func (s *importTraversal) readGlobMatches(directory, pattern string, location Location) ([]string, []Diagnostic) { canonical, err := s.access.canonical(directory) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, nil } return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be canonicalized", location)} } if !s.access.contains(canonical) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory resolves outside the configuration root", &location)} } if _, err := s.access.relative(canonical); err != nil { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory could not be addressed inside the configuration root", &location)} } var matches []string var problem *Diagnostic err = s.access.readDirectory(canonical, func(entry fs.DirEntry) bool { s.accounting.entries++ if s.accounting.entries > s.limits.entries { value := importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location) problem = &value return false } matched, matchErr := path.Match(pattern, entry.Name()) if matchErr != nil { value := importDiagnostic(CodeImportInvalid, "import glob pattern is invalid", &location) problem = &value return false } if matched { matches = append(matches, entry.Name()) } return true }) if problem != nil { return nil, []Diagnostic{*problem} } if errors.Is(err, fs.ErrNotExist) { return nil, nil } if err != nil { return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be traversed", location)} } slices.Sort(matches) 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) }