package configkit import ( "errors" "io" "io/fs" "os" "path" "path/filepath" "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 := filepath.Join(candidate.absolute, filepath.FromSlash(name)) relative := path.Join(candidate.relative, name) if !containedPath(s.rootPath, filepath.Clean(absolute)) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal is outside the configuration root", &location)} } canonical, err := canonicalExistingPath(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 !containedPath(s.rootPath, 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 len(s.sources)+len(next) > s.limits.sources { return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob matches exceed the remaining source-count limit", &location)} } continue } info, err := os.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 := canonicalExistingPath(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 !containedPath(s.rootPath, canonical) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory resolves outside the configuration root", &location)} } relative, err := filepath.Rel(s.rootPath, canonical) if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory could not be addressed inside the configuration root", &location)} } directoryFile, err := s.root.Open(relative) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, nil } return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be opened", location)} } var matches []string for { entries, readErr := directoryFile.ReadDir(globReadBatch) for _, entry := range entries { s.usedEntries++ if s.usedEntries > s.limits.entries { _ = directoryFile.Close() return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)} } matched, matchErr := path.Match(pattern, entry.Name()) if matchErr != nil { _ = directoryFile.Close() return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "import glob pattern is invalid", &location)} } if matched { matches = append(matches, entry.Name()) } } if errors.Is(readErr, io.EOF) { break } if readErr != nil { _ = directoryFile.Close() return nil, []Diagnostic{globFilesystemDiagnostic(readErr, "glob directory could not be read", location)} } } if err := directoryFile.Close(); err != nil { return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be closed", 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) }