package collector import ( "os" "path/filepath" "strings" ) // Matcher determines if a file should be included in the collection type Matcher interface { ShouldInclude(path string, info os.FileInfo) bool } // NameMatcher matches files by exact name type NameMatcher struct { name string } // NewNameMatcher creates a matcher for exact filename matching func NewNameMatcher(name string) *NameMatcher { return &NameMatcher{name: name} } // ShouldInclude returns true if the file matches the exact name func (m *NameMatcher) ShouldInclude(path string, info os.FileInfo) bool { if info.IsDir() { return false } return info.Name() == m.name } // PatternMatcher matches files within directories matching a glob pattern type PatternMatcher struct { pattern string matchedDirs map[string]bool patternSegments []string } // NewPatternMatcher creates a matcher for directory pattern matching func NewPatternMatcher(pattern string) *PatternMatcher { // Remove trailing slash if present pattern = strings.TrimSuffix(pattern, "/") return &PatternMatcher{ pattern: pattern, matchedDirs: make(map[string]bool), patternSegments: strings.Split(pattern, string(os.PathSeparator)), } } // ShouldInclude returns true if the file is within a directory matching the pattern func (m *PatternMatcher) ShouldInclude(path string, info os.FileInfo) bool { // For directories, check if they match the pattern and cache the result if info.IsDir() { matched, err := m.dirMatchesPattern(path) if err == nil && matched { m.matchedDirs[path] = true } return false // Don't include the directory itself, only files within } // For files, check if any parent directory is in the matched set dir := filepath.Dir(path) for { if m.matchedDirs[dir] { return true } // Also check if this directory matches the pattern (in case we haven't seen it yet) if matched, err := m.dirMatchesPattern(dir); err == nil && matched { m.matchedDirs[dir] = true return true } parent := filepath.Dir(dir) if parent == dir || parent == "." { break } dir = parent } return false } // dirMatchesPattern checks if a directory path matches the glob pattern func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) { // Get the directory name dirName := filepath.Base(dirPath) // For simple patterns (no path separators), just match the directory name if len(m.patternSegments) == 1 { return filepath.Match(m.pattern, dirName) } // For complex patterns, we need to match the full path segments pathSegments := strings.Split(dirPath, string(os.PathSeparator)) // Try to match the pattern segments against the path segments if len(pathSegments) < len(m.patternSegments) { return false, nil } // Check each pattern segment against the corresponding path segment for i := 0; i < len(m.patternSegments); i++ { // Start from the end of both slices patternIdx := len(m.patternSegments) - 1 - i pathIdx := len(pathSegments) - 1 - i matched, err := filepath.Match(m.patternSegments[patternIdx], pathSegments[pathIdx]) if err != nil { return false, err } if !matched { return false, nil } } return true, nil } // CompositeMatcher combines multiple matchers with OR logic type CompositeMatcher struct { matchers []Matcher } // NewCompositeMatcher creates a matcher that combines multiple matchers func NewCompositeMatcher(matchers []Matcher) *CompositeMatcher { return &CompositeMatcher{matchers: matchers} } // ShouldInclude returns true if ANY of the matchers match the file func (m *CompositeMatcher) ShouldInclude(path string, info os.FileInfo) bool { for _, matcher := range m.matchers { if matcher.ShouldInclude(path, info) { return true } } return false }