package meta import ( "fmt" "regexp" "strings" ) // MaxPatternLen — предел длины выражения (RE2 линеен, но огромные выражения бессмысленны). const MaxPatternLen = 1000 // Группы шаблона и поля, которые они заполняют. var knownGroups = map[string]bool{ "artist": true, "albumartist": true, "album": true, "year": true, "disc": true, "track": true, "title": true, "genre": true, "skip": true, } // PatternError — ошибка компиляции выражения (для показа под полем ввода). type PatternError struct { Index int Reason string } func (e *PatternError) Error() string { return fmt.Sprintf("pattern %d: %s", e.Index, e.Reason) } // Pattern — скомпилированный шаблон пути. type Pattern struct { Source string re *regexp.Regexp } // CompilePatterns проверяет и компилирует выражения по порядку. func CompilePatterns(exprs []string) ([]Pattern, error) { out := make([]Pattern, 0, len(exprs)) for i, e := range exprs { if len(e) > MaxPatternLen { return nil, &PatternError{Index: i, Reason: fmt.Sprintf("longer than %d characters", MaxPatternLen)} } re, err := regexp.Compile(e) if err != nil { return nil, &PatternError{Index: i, Reason: err.Error()} } named := 0 for _, name := range re.SubexpNames() { if name == "" { continue } if !knownGroups[name] { return nil, &PatternError{Index: i, Reason: fmt.Sprintf("unknown group %q", name)} } named++ } if named == 0 { return nil, &PatternError{Index: i, Reason: "no named groups"} } out = append(out, Pattern{Source: e, re: re}) } return out, nil } // PathMatch — результат применения шаблонов к пути. type PathMatch struct { Index int // номер совпавшего выражения, -1 — ни одно Fields map[string]string // извлечённые группы (непустые) Skip bool // совпала группа skip — файл игнорируется } // MatchPath применяет шаблоны к пути относительно корня источника (разделитель `/`). // Побеждает первое совпавшее выражение. func MatchPath(patterns []Pattern, relPath string) PathMatch { for i, p := range patterns { m := p.re.FindStringSubmatch(relPath) if m == nil { continue } // Выражение с группой skip описывает игнорируемые файлы: совпало — пропускаем. if p.re.SubexpIndex("skip") >= 0 { return PathMatch{Index: i, Skip: true} } fields := map[string]string{} for gi, name := range p.re.SubexpNames() { if name == "" { continue } if v := strings.TrimSpace(m[gi]); v != "" { fields[name] = v } } return PathMatch{Index: i, Fields: fields} } return PathMatch{Index: -1} }