mirror of
https://github.com/restic/restic.git
synced 2024-08-18 18:33:22 +02:00
330 lines
8.2 KiB
Go
330 lines
8.2 KiB
Go
package filter
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/restic/restic/internal/errors"
|
|
)
|
|
|
|
// ErrBadString is returned when Match is called with the empty string as the
|
|
// second argument.
|
|
var ErrBadString = errors.New("filter.Match: string is empty")
|
|
|
|
type patternPart struct {
|
|
pattern string // First is "/" for absolute pattern; "" for "**".
|
|
isSimple bool
|
|
}
|
|
|
|
// Pattern represents a preparsed filter pattern
|
|
type Pattern struct {
|
|
original string
|
|
parts []patternPart
|
|
isNegated bool
|
|
}
|
|
|
|
func prepareStr(str string) ([]string, error) {
|
|
if str == "" {
|
|
return nil, ErrBadString
|
|
}
|
|
return splitPath(str), nil
|
|
}
|
|
|
|
func preparePattern(patternStr string) Pattern {
|
|
var negate bool
|
|
|
|
originalPattern := patternStr
|
|
|
|
if patternStr[0] == '!' {
|
|
negate = true
|
|
patternStr = patternStr[1:]
|
|
}
|
|
|
|
pathParts := splitPath(filepath.Clean(patternStr))
|
|
parts := make([]patternPart, len(pathParts))
|
|
for i, part := range pathParts {
|
|
isSimple := !strings.ContainsAny(part, "\\[]*?")
|
|
// Replace "**" with the empty string to get faster comparisons
|
|
// (length-check only) in hasDoubleWildcard.
|
|
if part == "**" {
|
|
part = ""
|
|
}
|
|
parts[i] = patternPart{part, isSimple}
|
|
}
|
|
|
|
return Pattern{originalPattern, parts, negate}
|
|
}
|
|
|
|
// Split p into path components. Assuming p has been Cleaned, no component
|
|
// will be empty. For absolute paths, the first component is "/".
|
|
func splitPath(p string) []string {
|
|
parts := strings.Split(filepath.ToSlash(p), "/")
|
|
if parts[0] == "" {
|
|
parts[0] = "/"
|
|
}
|
|
return parts
|
|
}
|
|
|
|
// Match returns true if str matches the pattern. When the pattern is
|
|
// malformed, filepath.ErrBadPattern is returned. The empty pattern matches
|
|
// everything, when str is the empty string ErrBadString is returned.
|
|
//
|
|
// Pattern can be a combination of patterns suitable for filepath.Match, joined
|
|
// by filepath.Separator.
|
|
//
|
|
// In addition patterns suitable for filepath.Match, pattern accepts a
|
|
// recursive wildcard '**', which greedily matches an arbitrary number of
|
|
// intermediate directories.
|
|
func Match(patternStr, str string) (matched bool, err error) {
|
|
if patternStr == "" {
|
|
return true, nil
|
|
}
|
|
|
|
pattern := preparePattern(patternStr)
|
|
strs, err := prepareStr(str)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return match(pattern, strs)
|
|
}
|
|
|
|
// ChildMatch returns true if children of str can match the pattern. When the pattern is
|
|
// malformed, filepath.ErrBadPattern is returned. The empty pattern matches
|
|
// everything, when str is the empty string ErrBadString is returned.
|
|
//
|
|
// Pattern can be a combination of patterns suitable for filepath.Match, joined
|
|
// by filepath.Separator.
|
|
//
|
|
// In addition patterns suitable for filepath.Match, pattern accepts a
|
|
// recursive wildcard '**', which greedily matches an arbitrary number of
|
|
// intermediate directories.
|
|
func ChildMatch(patternStr, str string) (matched bool, err error) {
|
|
if patternStr == "" {
|
|
return true, nil
|
|
}
|
|
|
|
pattern := preparePattern(patternStr)
|
|
strs, err := prepareStr(str)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return childMatch(pattern, strs)
|
|
}
|
|
|
|
func childMatch(pattern Pattern, strs []string) (matched bool, err error) {
|
|
if pattern.parts[0].pattern != "/" {
|
|
// relative pattern can always be nested down
|
|
return true, nil
|
|
}
|
|
|
|
ok, pos := hasDoubleWildcard(pattern)
|
|
if ok && len(strs) >= pos {
|
|
// cut off at the double wildcard
|
|
strs = strs[:pos]
|
|
}
|
|
|
|
// match path against absolute pattern prefix
|
|
l := 0
|
|
if len(strs) > len(pattern.parts) {
|
|
l = len(pattern.parts)
|
|
} else {
|
|
l = len(strs)
|
|
}
|
|
return match(Pattern{pattern.original, pattern.parts[0:l], pattern.isNegated}, strs)
|
|
}
|
|
|
|
func hasDoubleWildcard(list Pattern) (ok bool, pos int) {
|
|
for i, item := range list.parts {
|
|
if item.pattern == "" {
|
|
return true, i
|
|
}
|
|
}
|
|
|
|
return false, 0
|
|
}
|
|
|
|
func match(pattern Pattern, strs []string) (matched bool, err error) {
|
|
if ok, pos := hasDoubleWildcard(pattern); ok {
|
|
// gradually expand '**' into separate wildcards
|
|
newPat := make([]patternPart, len(strs))
|
|
// copy static prefix once
|
|
copy(newPat, pattern.parts[:pos])
|
|
for i := 0; i <= len(strs)-len(pattern.parts)+1; i++ {
|
|
// limit to static prefix and already appended '*'
|
|
newPat := newPat[:pos+i]
|
|
// in the first iteration the wildcard expands to nothing
|
|
if i > 0 {
|
|
newPat[pos+i-1] = patternPart{"*", false}
|
|
}
|
|
newPat = append(newPat, pattern.parts[pos+1:]...)
|
|
|
|
matched, err := match(Pattern{pattern.original, newPat, pattern.isNegated}, strs)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if matched {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
if len(pattern.parts) == 0 && len(strs) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// an empty pattern never matches a non-empty path
|
|
if len(pattern.parts) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
if len(pattern.parts) <= len(strs) {
|
|
minOffset := 0
|
|
maxOffset := len(strs) - len(pattern.parts)
|
|
// special case absolute patterns
|
|
if pattern.parts[0].pattern == "/" {
|
|
maxOffset = 0
|
|
} else if strs[0] == "/" {
|
|
// skip absolute path marker if pattern is not rooted
|
|
minOffset = 1
|
|
}
|
|
outer:
|
|
for offset := maxOffset; offset >= minOffset; offset-- {
|
|
|
|
for i := len(pattern.parts) - 1; i >= 0; i-- {
|
|
var ok bool
|
|
if pattern.parts[i].isSimple {
|
|
ok = pattern.parts[i].pattern == strs[offset+i]
|
|
} else {
|
|
ok, err = filepath.Match(pattern.parts[i].pattern, strs[offset+i])
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "Match")
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
continue outer
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
type InvalidPatternError struct {
|
|
InvalidPatterns []string
|
|
}
|
|
|
|
func (e *InvalidPatternError) Error() string {
|
|
return "invalid pattern(s) provided:\n" + strings.Join(e.InvalidPatterns, "\n")
|
|
}
|
|
|
|
// ValidatePatterns validates a slice of patterns.
|
|
// Returns true if all patterns are valid - false otherwise, along with the invalid patterns.
|
|
func ValidatePatterns(patterns []string) error {
|
|
invalidPatterns := make([]string, 0)
|
|
|
|
for _, Pattern := range ParsePatterns(patterns) {
|
|
// Validate all pattern parts
|
|
for _, part := range Pattern.parts {
|
|
// Validate the pattern part by trying to match it against itself
|
|
if _, validErr := filepath.Match(part.pattern, part.pattern); validErr != nil {
|
|
invalidPatterns = append(invalidPatterns, Pattern.original)
|
|
|
|
// If a single part is invalid, stop processing this pattern
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(invalidPatterns) > 0 {
|
|
return &InvalidPatternError{InvalidPatterns: invalidPatterns}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParsePatterns prepares a list of patterns for use with List.
|
|
func ParsePatterns(pattern []string) []Pattern {
|
|
patpat := make([]Pattern, 0)
|
|
for _, pat := range pattern {
|
|
if pat == "" {
|
|
continue
|
|
}
|
|
|
|
pats := preparePattern(pat)
|
|
patpat = append(patpat, pats)
|
|
}
|
|
return patpat
|
|
}
|
|
|
|
// List returns true if str matches one of the patterns. Empty patterns are ignored.
|
|
func List(patterns []Pattern, str string) (matched bool, err error) {
|
|
matched, _, err = list(patterns, false, str)
|
|
return matched, err
|
|
}
|
|
|
|
// ListWithChild returns true if str matches one of the patterns. Empty patterns are ignored.
|
|
func ListWithChild(patterns []Pattern, str string) (matched bool, childMayMatch bool, err error) {
|
|
return list(patterns, true, str)
|
|
}
|
|
|
|
// list returns true if str matches one of the patterns. Empty patterns are ignored.
|
|
// Patterns prefixed by "!" are negated: any matching file excluded by a previous pattern
|
|
// will become included again.
|
|
func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, childMayMatch bool, err error) {
|
|
if len(patterns) == 0 {
|
|
return false, false, nil
|
|
}
|
|
|
|
strs, err := prepareStr(str)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
|
|
hasNegatedPattern := false
|
|
for _, pat := range patterns {
|
|
hasNegatedPattern = hasNegatedPattern || pat.isNegated
|
|
}
|
|
|
|
for _, pat := range patterns {
|
|
m, err := match(pat, strs)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
|
|
var c bool
|
|
if checkChildMatches {
|
|
c, err = childMatch(pat, strs)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
} else {
|
|
c = true
|
|
}
|
|
|
|
if pat.isNegated {
|
|
matched = matched && !m
|
|
childMayMatch = childMayMatch && !m
|
|
} else {
|
|
matched = matched || m
|
|
childMayMatch = childMayMatch || c
|
|
|
|
if matched && childMayMatch && !hasNegatedPattern {
|
|
// without negative patterns the result cannot change any more
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return matched, childMayMatch, nil
|
|
}
|