matchers: Parse Matcher now expects consistent enclosing with quotes. (#2632)

Fixes https://github.com/prometheus/alertmanager/issues/2630

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>
This commit is contained in:
Bartlomiej Plotka 2021-06-23 11:05:49 +02:00 committed by GitHub
parent 29fcb0b7fb
commit 02346e4e49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 32 deletions

View File

@ -22,11 +22,9 @@ import (
) )
var ( var (
re = regexp.MustCompile( // '=~' has to come before '=' because otherwise only the '='
// '=~' has to come before '=' because otherwise only the '=' // will be consumed, and the '~' will be part of the 3rd token.
// will be consumed, and the '~' will be part of the 3rd token. re = regexp.MustCompile(`^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(=~|=|!=|!~)\s*((?s).*?)\s*$`)
`^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(=~|=|!=|!~)\s*((?s).*?)\s*$`,
)
typeMap = map[string]MatchType{ typeMap = map[string]MatchType{
"=": MatchEqual, "=": MatchEqual,
"!=": MatchNotEqual, "!=": MatchNotEqual,
@ -116,20 +114,26 @@ func ParseMatchers(s string) ([]*Matcher, error) {
// character). However, literal line feed characters are tolerated, as are // character). However, literal line feed characters are tolerated, as are
// single '\' characters not followed by '\', 'n', or '"'. They act as a literal // single '\' characters not followed by '\', 'n', or '"'. They act as a literal
// backslash in that case. // backslash in that case.
func ParseMatcher(s string) (*Matcher, error) { func ParseMatcher(s string) (_ *Matcher, err error) {
ms := re.FindStringSubmatch(s) ms := re.FindStringSubmatch(s)
if len(ms) == 0 { if len(ms) == 0 {
return nil, errors.Errorf("bad matcher format: %s", s) return nil, errors.Errorf("bad matcher format: %s", s)
} }
var ( var (
rawValue = strings.TrimPrefix(ms[3], "\"") rawValue = ms[3]
value strings.Builder value strings.Builder
escaped bool escaped bool
expectTrailingQuote bool
) )
if rawValue[0] == '"' {
rawValue = strings.TrimPrefix(rawValue, "\"")
expectTrailingQuote = true
}
if !utf8.ValidString(rawValue) { if !utf8.ValidString(rawValue) {
return nil, errors.Errorf("matcher value not valid UTF-8: %s", rawValue) return nil, errors.Errorf("matcher value not valid UTF-8: %s", ms[3])
} }
// Unescape the rawValue: // Unescape the rawValue:
@ -157,15 +161,18 @@ func ParseMatcher(s string) (*Matcher, error) {
// '\' encountered as last byte. Treat it as literal. // '\' encountered as last byte. Treat it as literal.
value.WriteByte('\\') value.WriteByte('\\')
case '"': case '"':
if i < len(rawValue)-1 { // Otherwise this is a trailing quote. if !expectTrailingQuote || i < len(rawValue)-1 {
return nil, errors.Errorf( return nil, errors.Errorf("matcher value contains unescaped double quote: %s", ms[3])
"matcher value contains unescaped double quote: %s", rawValue,
)
} }
expectTrailingQuote = false
default: default:
value.WriteRune(r) value.WriteRune(r)
} }
} }
if expectTrailingQuote {
return nil, errors.Errorf("matcher value contains unescaped double quote: %s", ms[3])
}
return NewMatcher(typeMap[ms[2]], ms[1], value.String()) return NewMatcher(typeMap[ms[2]], ms[1], value.String())
} }

View File

@ -19,7 +19,7 @@ import (
) )
func TestMatchers(t *testing.T) { func TestMatchers(t *testing.T) {
testCases := []struct { for _, tc := range []struct {
input string input string
want []*Matcher want []*Matcher
err string err string
@ -173,7 +173,7 @@ func TestMatchers(t *testing.T) {
}(), }(),
}, },
{ {
input: `trickier==\\=\=\""`, input: `trickier==\\=\=\"`,
want: func() []*Matcher { want: func() []*Matcher {
ms := []*Matcher{} ms := []*Matcher{}
m, _ := NewMatcher(MatchEqual, "trickier", `=\=\="`) m, _ := NewMatcher(MatchEqual, "trickier", `=\=\="`)
@ -189,6 +189,18 @@ func TestMatchers(t *testing.T) {
return append(ms, m, m2) return append(ms, m, m2)
}(), }(),
}, },
{
input: `job="value`,
err: `matcher value contains unescaped double quote: "value`,
},
{
input: `job=value"`,
err: `matcher value contains unescaped double quote: value"`,
},
{
input: `trickier==\\=\=\""`,
err: `matcher value contains unescaped double quote: =\\=\=\""`,
},
{ {
input: `contains_unescaped_quote = foo"bar`, input: `contains_unescaped_quote = foo"bar`,
err: `matcher value contains unescaped double quote: foo"bar`, err: `matcher value contains unescaped double quote: foo"bar`,
@ -201,22 +213,47 @@ func TestMatchers(t *testing.T) {
input: `{foo=~"invalid[regexp"}`, input: `{foo=~"invalid[regexp"}`,
err: "error parsing regexp: missing closing ]: `[regexp)$`", err: "error parsing regexp: missing closing ]: `[regexp)$`",
}, },
} // Double escaped strings.
{
for i, tc := range testCases { input: `"{foo=\"bar"}`,
got, err := ParseMatchers(tc.input) err: `bad matcher format: "{foo=\"bar"`,
if err != nil && tc.err == "" { },
t.Fatalf("got error where none expected (i=%d): %v", i, err) {
} input: `"foo=\"bar"`,
if err == nil && tc.err != "" { err: `bad matcher format: "foo=\"bar"`,
t.Fatalf("expected error but got none (i=%d): %v", i, tc.err) },
} {
if err != nil && err.Error() != tc.err { input: `"foo=\"bar\""`,
t.Fatalf("error not equal (i=%d):\ngot %v\nwant %v", i, err, tc.err) err: `bad matcher format: "foo=\"bar\""`,
} },
if !reflect.DeepEqual(got, tc.want) { {
t.Fatalf("labels not equal (i=%d):\ngot %v\nwant %v", i, got, tc.want) input: `"foo=\"bar\"`,
} err: `bad matcher format: "foo=\"bar\"`,
},
{
input: `"{foo=\"bar\"}"`,
err: `bad matcher format: "{foo=\"bar\"}"`,
},
{
input: `"foo="bar""`,
err: `bad matcher format: "foo="bar""`,
},
} {
t.Run(tc.input, func(t *testing.T) {
got, err := ParseMatchers(tc.input)
if err != nil && tc.err == "" {
t.Fatalf("got error where none expected: %v", err)
}
if err == nil && tc.err != "" {
t.Fatalf("expected error but got none: %v", tc.err)
}
if err != nil && err.Error() != tc.err {
t.Fatalf("error not equal:\ngot %v\nwant %v", err, tc.err)
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("labels not equal:\ngot %v\nwant %v", got, tc.want)
}
})
} }
} }