summary refs log tree commit diff
path: root/vendor/go.mau.fi/util/glob
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/go.mau.fi/util/glob')
-rw-r--r--vendor/go.mau.fi/util/glob/glob.go100
-rw-r--r--vendor/go.mau.fi/util/glob/regex.go43
-rw-r--r--vendor/go.mau.fi/util/glob/simple.go62
-rw-r--r--vendor/go.mau.fi/util/glob/util.go42
4 files changed, 247 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/util/glob/glob.go b/vendor/go.mau.fi/util/glob/glob.go
new file mode 100644
index 0000000..67fce3e
--- /dev/null
+++ b/vendor/go.mau.fi/util/glob/glob.go
@@ -0,0 +1,100 @@
+// Package glob implements very simple glob pattern matching used in various parts of the Matrix spec,
+// such as push rules and moderation policy lists.
+//
+// See https://spec.matrix.org/v1.11/appendices/#glob-style-matching for more info.
+package glob
+
+import (
+	"strings"
+)
+
+type Glob interface {
+	Match(string) bool
+}
+
+var (
+	_ Glob = ExactGlob("")
+	_ Glob = PrefixGlob("")
+	_ Glob = SuffixGlob("")
+	_ Glob = ContainsGlob("")
+	_ Glob = (*PrefixAndSuffixGlob)(nil)
+	_ Glob = (*PrefixSuffixAndContainsGlob)(nil)
+	_ Glob = (*RegexGlob)(nil)
+)
+
+// Compile compiles a glob pattern into an object that can be used to efficiently match strings against the pattern.
+//
+// Simple globs will be converted into prefix/suffix/contains checks, while complicated ones will be compiled as regex.
+func Compile(pattern string) Glob {
+	pattern = Simplify(pattern)
+	g := compileSimple(pattern)
+	if g != nil {
+		return g
+	}
+	g, _ = CompileRegex(pattern)
+	return g
+}
+
+// CompileWithImplicitContains is a wrapper for Compile which will replace exact matches with contains matches.
+// i.e. if the pattern has no wildcards, it will be treated as if it was surrounded in asterisks (`foo` -> `*foo*`).
+func CompileWithImplicitContains(pattern string) Glob {
+	g := Compile(pattern)
+	if _, isExact := g.(ExactGlob); isExact {
+		return ContainsGlob(pattern)
+	}
+	return g
+}
+
+// CompileSimple compiles a glob pattern into one of the non-regex forms.
+//
+// If the pattern can't be compiled into a simple form, it returns nil.
+func CompileSimple(pattern string) Glob {
+	return compileSimple(Simplify(pattern))
+}
+
+func compileSimple(pattern string) Glob {
+	if strings.ContainsRune(pattern, '?') {
+		return nil
+	}
+	switch strings.Count(pattern, "*") {
+	case 0:
+		return ExactGlob(pattern)
+	case 1:
+		if strings.HasPrefix(pattern, "*") {
+			return SuffixGlob(pattern[1:])
+		} else if strings.HasSuffix(pattern, "*") {
+			return PrefixGlob(pattern[:len(pattern)-1])
+		} else {
+			parts := strings.Split(pattern, "*")
+			return PrefixAndSuffixGlob{
+				Prefix: parts[0],
+				Suffix: parts[1],
+			}
+		}
+	case 2:
+		if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
+			return ContainsGlob(pattern[1 : len(pattern)-1])
+		}
+		parts := strings.Split(pattern, "*")
+		return PrefixSuffixAndContainsGlob{
+			Prefix:   parts[0],
+			Contains: parts[1],
+			Suffix:   parts[2],
+		}
+	default:
+		return nil
+	}
+}
+
+var sqlCompiler = strings.NewReplacer(
+	`\`, `\\`,
+	`%`, `\%`,
+	`_`, `\_`,
+	`*`, `%`,
+	`?`, `_`,
+)
+
+// ToSQL converts a Matrix glob pattern to a SQL LIKE pattern.
+func ToSQL(pattern string) string {
+	return sqlCompiler.Replace(Simplify(pattern))
+}
diff --git a/vendor/go.mau.fi/util/glob/regex.go b/vendor/go.mau.fi/util/glob/regex.go
new file mode 100644
index 0000000..f224533
--- /dev/null
+++ b/vendor/go.mau.fi/util/glob/regex.go
@@ -0,0 +1,43 @@
+package glob
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+)
+
+type RegexGlob struct {
+	regex *regexp.Regexp
+}
+
+func (rg *RegexGlob) Match(s string) bool {
+	return rg.regex.MatchString(s)
+}
+
+func CompileRegex(pattern string) (*RegexGlob, error) {
+	var buf strings.Builder
+	buf.WriteRune('^')
+	for _, part := range SplitPattern(pattern) {
+		if strings.ContainsRune(part, '*') || strings.ContainsRune(part, '?') {
+			questions := strings.Count(part, "?")
+			star := strings.ContainsRune(part, '*')
+			if star {
+				if questions > 0 {
+					_, _ = fmt.Fprintf(&buf, ".{%d,}", questions)
+				} else {
+					buf.WriteString(".*")
+				}
+			} else if questions > 0 {
+				_, _ = fmt.Fprintf(&buf, ".{%d}", questions)
+			}
+		} else {
+			buf.WriteString(regexp.QuoteMeta(part))
+		}
+	}
+	buf.WriteRune('$')
+	regex, err := regexp.Compile(buf.String())
+	if err != nil {
+		return nil, err
+	}
+	return &RegexGlob{regex}, nil
+}
diff --git a/vendor/go.mau.fi/util/glob/simple.go b/vendor/go.mau.fi/util/glob/simple.go
new file mode 100644
index 0000000..bb9bf6d
--- /dev/null
+++ b/vendor/go.mau.fi/util/glob/simple.go
@@ -0,0 +1,62 @@
+package glob
+
+import (
+	"strings"
+)
+
+// ExactGlob is the result of [Compile] when the pattern contains no glob characters.
+// It uses a simple string comparison to match.
+type ExactGlob string
+
+func (eg ExactGlob) Match(s string) bool {
+	return string(eg) == s
+}
+
+// SuffixGlob is the result of [Compile] when the pattern only has one `*` at the beginning.
+// It uses [strings.HasSuffix] to match.
+type SuffixGlob string
+
+func (sg SuffixGlob) Match(s string) bool {
+	return strings.HasSuffix(s, string(sg))
+}
+
+// PrefixGlob is the result of [Compile] when the pattern only has one `*` at the end.
+// It uses [strings.HasPrefix] to match.
+type PrefixGlob string
+
+func (pg PrefixGlob) Match(s string) bool {
+	return strings.HasPrefix(s, string(pg))
+}
+
+// ContainsGlob is the result of [Compile] when the pattern has two `*`s, one at the beginning and one at the end.
+// It uses [strings.Contains] to match.
+//
+// When there are exactly two `*`s, but they're not surrounding the string, the pattern is compiled as a [PrefixSuffixAndContainsGlob] instead.
+type ContainsGlob string
+
+func (cg ContainsGlob) Match(s string) bool {
+	return strings.Contains(s, string(cg))
+}
+
+// PrefixAndSuffixGlob is the result of [Compile] when the pattern only has one `*` in the middle.
+type PrefixAndSuffixGlob struct {
+	Prefix string
+	Suffix string
+}
+
+func (psg PrefixAndSuffixGlob) Match(s string) bool {
+	return strings.HasPrefix(s, psg.Prefix) && strings.HasSuffix(s[len(psg.Prefix):], psg.Suffix)
+}
+
+// PrefixSuffixAndContainsGlob is the result of [Compile] when the pattern has two `*`s which are not surrounding the rest of the pattern.
+type PrefixSuffixAndContainsGlob struct {
+	Prefix   string
+	Suffix   string
+	Contains string
+}
+
+func (psacg PrefixSuffixAndContainsGlob) Match(s string) bool {
+	return strings.HasPrefix(s, psacg.Prefix) &&
+		strings.HasSuffix(s[len(psacg.Prefix):], psacg.Suffix) &&
+		strings.Contains(s[len(psacg.Prefix):len(s)-len(psacg.Suffix)], psacg.Contains)
+}
diff --git a/vendor/go.mau.fi/util/glob/util.go b/vendor/go.mau.fi/util/glob/util.go
new file mode 100644
index 0000000..37d9b2d
--- /dev/null
+++ b/vendor/go.mau.fi/util/glob/util.go
@@ -0,0 +1,42 @@
+package glob
+
+import (
+	"regexp"
+	"strings"
+)
+
+var redundantStarRegex = regexp.MustCompile(`\*{2,}`)
+var maybeRedundantQuestionRegex = regexp.MustCompile(`[*?]{2,}`)
+var wildcardRegex = regexp.MustCompile(`[*?]+`)
+
+func SplitPattern(pattern string) []string {
+	indexes := wildcardRegex.FindAllStringIndex(pattern, -1)
+	if len(indexes) == 0 {
+		return []string{pattern}
+	}
+	parts := make([]string, 0, len(indexes)+1)
+	start := 0
+	for _, part := range indexes {
+		end := part[0]
+		if end > start {
+			parts = append(parts, pattern[start:end])
+		}
+		parts = append(parts, pattern[part[0]:part[1]])
+		start = part[1]
+	}
+	if start < len(pattern) {
+		parts = append(parts, pattern[start:])
+	}
+	return parts
+}
+
+func Simplify(pattern string) string {
+	pattern = redundantStarRegex.ReplaceAllString(pattern, "*")
+	pattern = maybeRedundantQuestionRegex.ReplaceAllStringFunc(pattern, func(s string) string {
+		if !strings.ContainsRune(s, '*') {
+			return s
+		}
+		return strings.Repeat("?", strings.Count(s, "?")) + "*"
+	})
+	return pattern
+}