diff --git a/pkg/labels/regexp.go b/pkg/labels/regexp.go index 2c9f7616da..532a548062 100644 --- a/pkg/labels/regexp.go +++ b/pkg/labels/regexp.go @@ -24,10 +24,11 @@ const maxSetMatches = 256 type FastRegexMatcher struct { re *regexp.Regexp - setMatches []string - prefix string - suffix string - contains string + setMatches []string + stringMatcher StringMatcher + prefix string + suffix string + contains string } func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) { @@ -42,8 +43,9 @@ func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) { return nil, err } m := &FastRegexMatcher{ - re: re, - setMatches: findSetMatches(parsed, ""), + re: re, + setMatches: findSetMatches(parsed, ""), + stringMatcher: stringMatcherFromRegexp(parsed), } if parsed.Op == syntax.OpConcat { @@ -193,6 +195,9 @@ func (m *FastRegexMatcher) MatchString(s string) bool { } return false } + if m.stringMatcher != nil { + return m.stringMatcher.Matches(s) + } if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { return false } @@ -262,18 +267,17 @@ func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { clearCapture(re) clearBeginEndText(re) switch re.Op { - case syntax.OpStar: + case syntax.OpPlus, syntax.OpStar: + if re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL { + return nil + } return anyStringMatcher{ - allowEmpty: true, - matchNL: re.Flags&syntax.DotNL != 0, + allowEmpty: re.Op == syntax.OpStar, + matchNL: re.Sub[0].Op == syntax.OpAnyChar, } case syntax.OpEmptyMatch: return emptyStringMatcher{} - case syntax.OpPlus: - return anyStringMatcher{ - allowEmpty: false, - matchNL: re.Flags&syntax.DotNL != 0, - } + case syntax.OpLiteral: return equalStringMatcher{ s: string(re.Rune), @@ -328,9 +332,9 @@ func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { } if len(matches) > 0 { return containsStringMatcher{ - substr: matches, - left: left, - right: right, + substrings: matches, + left: left, + right: right, } } } @@ -338,32 +342,32 @@ func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { } type containsStringMatcher struct { - substr []string - left StringMatcher - right StringMatcher + substrings []string + left StringMatcher + right StringMatcher } func (m containsStringMatcher) Matches(s string) bool { var pos int - for _, substr := range m.substr { + for _, substr := range m.substrings { pos = strings.Index(s, substr) if pos < 0 { continue } if m.right != nil && m.left != nil { - if m.left.Matches(s[:pos]) && m.right.Matches(s[pos+len(m.substr):]) { + if m.left.Matches(s[:pos]) && m.right.Matches(s[pos+len(substr):]) { return true } continue } if m.left != nil { - if m.left.Matches(s[:pos]) { + if pos+len(substr) == len(s) && m.left.Matches(s[:pos]) { return true } continue } if m.right != nil { - if m.right.Matches(s[pos+len(m.substr):]) { + if pos == 0 && m.right.Matches(s[pos+len(substr):]) { return true } continue @@ -395,7 +399,7 @@ type equalStringMatcher struct { } func (m equalStringMatcher) Matches(s string) bool { - if !m.caseSensitive { + if m.caseSensitive { return m.s == s } return strings.EqualFold(m.s, s) diff --git a/pkg/labels/regexp_test.go b/pkg/labels/regexp_test.go index 4510f1694b..0380fff388 100644 --- a/pkg/labels/regexp_test.go +++ b/pkg/labels/regexp_test.go @@ -48,14 +48,17 @@ var ( "(?s:.*)", "(?s:.+)", "(?s:^.*foo$)", + "^(?i:foo|oo)|(bar)$", "((.*)(bar|b|buzz)(.+)|foo)$", "^$", "(prometheus|api_prom)_api_v1_.+", "10\\.0\\.(1|2)\\.+", + "10\\.0\\.(1|2).+", + "((fo(bar))|.+foo)", } values = []string{ "foo", " foo bar", "bar", "buzz\nbar", "bar foo", "bfoo", "\n", "\nfoo", "foo\n", "hello foo world", "hello foo\n world", "", - "\nfoo\n", strings.Repeat("f", 20), "prometheus", "prometheus_api_v1", "prometheus_api_v1_foo", + "FOO", "Foo", "OO", "Oo", "\nfoo\n", strings.Repeat("f", 20), "prometheus", "prometheus_api_v1", "prometheus_api_v1_foo", "10.0.1.20", "10.0.2.10", "10.0.3.30", "10.0.4.40", } ) @@ -203,6 +206,7 @@ func Test_OptimizeRegex(t *testing.T) { exp StringMatcher }{ {".*", anyStringMatcher{allowEmpty: true, matchNL: false}}, + {".*?", anyStringMatcher{allowEmpty: true, matchNL: false}}, {"(?s:.*)", anyStringMatcher{allowEmpty: true, matchNL: true}}, {"(.*)", anyStringMatcher{allowEmpty: true, matchNL: false}}, {"^.*$", anyStringMatcher{allowEmpty: true, matchNL: false}}, @@ -216,22 +220,23 @@ func Test_OptimizeRegex(t *testing.T) { {"^(?i:foo)$", equalStringMatcher{s: "FOO", caseSensitive: false}}, {"^(?i:foo)|(bar)$", orStringMatcher([]StringMatcher{equalStringMatcher{s: "FOO", caseSensitive: false}, equalStringMatcher{s: "bar", caseSensitive: true}})}, {"^(?i:foo|oo)|(bar)$", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{equalStringMatcher{s: "FOO", caseSensitive: false}, equalStringMatcher{s: "OO", caseSensitive: false}}), equalStringMatcher{s: "bar", caseSensitive: true}})}, - {".*foo.*", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"(.*)foo.*", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"(.*)foo(.*)", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"(.+)foo(.*)", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"^.+foo.+", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, - {"^(.*)(foo)(.*)$", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"^(.*)(foo|foobar)(.*)$", containsStringMatcher{substr: []string{"foo", "foobar"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"^(.*)(foo|foobar)(.+)$", containsStringMatcher{substr: []string{"foo", "foobar"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, - {"^(.*)(bar|b|buzz)(.+)$", containsStringMatcher{substr: []string{"bar", "b", "buzz"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, - {"10\\.0\\.(1|2)\\.+", containsStringMatcher{substr: []string{"10.0.1", "10.0.2"}, left: nil, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, - {"^.+foo", containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}}, - {"foo-.*$", containsStringMatcher{substr: []string{"foo-"}, left: nil, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, - {"(prometheus|api_prom)_api_v1_.+", containsStringMatcher{substr: []string{"prometheus_api_v1_", "api_prom_api_v1_"}, left: nil, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, - {"^((.*)(bar|b|buzz)(.+)|foo)$", orStringMatcher([]StringMatcher{containsStringMatcher{substr: []string{"bar", "b", "buzz"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}, equalStringMatcher{s: "foo", caseSensitive: true}})}, - {"((fo(bar))|.+foo)", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{equalStringMatcher{s: "fobar", caseSensitive: true}}), containsStringMatcher{substr: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}})}, - {"(.+)/(gateway|cortex-gw|cortex-gw-internal)", containsStringMatcher{substr: []string{"/gateway", "/cortex-gw", "/cortex-gw-internal"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}}, + {".*foo.*", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"(.*)foo.*", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"(.*)foo(.*)", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"(.+)foo(.*)", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"^.+foo.+", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, + {"^(.*)(foo)(.*)$", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"^(.*)(foo|foobar)(.*)$", containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"^(.*)(foo|foobar)(.+)$", containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, + {"^(.*)(bar|b|buzz)(.+)$", containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, + {"10\\.0\\.(1|2)\\.+", nil}, + {"10\\.0\\.(1|2).+", containsStringMatcher{substrings: []string{"10.0.1", "10.0.2"}, left: nil, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, + {"^.+foo", containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}}, + {"foo-.*$", containsStringMatcher{substrings: []string{"foo-"}, left: nil, right: anyStringMatcher{allowEmpty: true, matchNL: false}}}, + {"(prometheus|api_prom)_api_v1_.+", containsStringMatcher{substrings: []string{"prometheus_api_v1_", "api_prom_api_v1_"}, left: nil, right: anyStringMatcher{allowEmpty: false, matchNL: false}}}, + {"^((.*)(bar|b|buzz)(.+)|foo)$", orStringMatcher([]StringMatcher{containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringMatcher{allowEmpty: true, matchNL: false}, right: anyStringMatcher{allowEmpty: false, matchNL: false}}, equalStringMatcher{s: "foo", caseSensitive: true}})}, + {"((fo(bar))|.+foo)", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{equalStringMatcher{s: "fobar", caseSensitive: true}}), containsStringMatcher{substrings: []string{"foo"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}})}, + {"(.+)/(gateway|cortex-gw|cortex-gw-internal)", containsStringMatcher{substrings: []string{"/gateway", "/cortex-gw", "/cortex-gw-internal"}, left: anyStringMatcher{allowEmpty: false, matchNL: false}, right: nil}}, // we don't support case insensitive matching for contains. // This is because there's no strings.IndexOfFold function. // We can revisit later if this is really popular by using strings.ToUpper.