internal/wildcard/matcher.go (107 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package wildcard
import (
"strings"
"unicode"
"unicode/utf8"
)
// CaseSensitivity controls the case sensitivity of matching.
type CaseSensitivity bool
// CaseSensitivity values.
const (
CaseSensitive CaseSensitivity = true
CaseInsensitive CaseSensitivity = false
)
// NewMatcher constructs a new wildcard matcher for the given pattern.
//
// If p is the empty string, it will match only the empty string.
// If p is not a valid UTF-8 string, matching behaviour is undefined.
func NewMatcher(p string, caseSensitive CaseSensitivity) *Matcher {
parts := strings.Split(p, "*")
m := &Matcher{
wildcardBegin: strings.HasPrefix(p, "*"),
wildcardEnd: strings.HasSuffix(p, "*"),
caseSensitive: caseSensitive,
}
for _, part := range parts {
if part == "" {
continue
}
if !m.caseSensitive {
part = strings.ToLower(part)
}
m.parts = append(m.parts, part)
}
return m
}
// Matcher matches strings against a wildcard pattern with configurable case sensitivity.
type Matcher struct {
parts []string
wildcardBegin bool
wildcardEnd bool
caseSensitive CaseSensitivity
}
// Match reports whether s matches m's wildcard pattern.
func (m *Matcher) Match(s string) bool {
if len(m.parts) == 0 && !m.wildcardBegin && !m.wildcardEnd {
return s == ""
}
if len(m.parts) == 1 && !m.wildcardBegin && !m.wildcardEnd {
if m.caseSensitive {
return s == m.parts[0]
}
return len(s) == len(m.parts[0]) && hasPrefixLower(s, m.parts[0]) == 0
}
parts := m.parts
if !m.wildcardEnd && len(parts) > 0 {
part := parts[len(parts)-1]
if m.caseSensitive {
if !strings.HasSuffix(s, part) {
return false
}
} else {
if len(s) < len(part) {
return false
}
if hasPrefixLower(s[len(s)-len(part):], part) != 0 {
return false
}
}
parts = parts[:len(parts)-1]
}
for i, part := range parts {
j := -1
if m.caseSensitive {
if i > 0 || m.wildcardBegin {
j = strings.Index(s, part)
} else {
if !strings.HasPrefix(s, part) {
return false
}
j = 0
}
} else {
off := 0
for j == -1 && len(s)-off >= len(part) {
skip := hasPrefixLower(s[off:], part)
if skip == 0 {
j = off
} else {
if i == 0 && !m.wildcardBegin {
return false
}
off += skip
}
}
}
if j == -1 {
return false
}
s = s[j+len(part):]
}
return true
}
// hasPrefixLower reports whether or not s begins with prefixLower,
// returning 0 if it does, and the number of bytes representing the
// first rune in s otherwise.
func hasPrefixLower(s, prefixLower string) (skip int) {
var firstSize int
for i, r := range prefixLower {
r2, size := utf8.DecodeRuneInString(s[i:])
if firstSize == 0 {
firstSize = size
}
if r2 != r && r2 != unicode.ToUpper(r) {
return firstSize
}
}
return 0
}