labels.Matcher: Improve regexp and allow escaping
This addresses a number of issues: - It was impossible to include a literal '"' or a line break in the value. - It was impossible to include '=', '~', or '!' in the value. - It was not validated if the label name is valid. - It was not validated if the value is valid UTF-8. - No whitespace was allowed around the operator. Signed-off-by: beorn7 <beorn@grafana.com>
This commit is contained in:
parent
e87985a9a8
commit
9bb7ab43cd
|
@ -16,12 +16,17 @@ package labels
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
re = regexp.MustCompile(`(?:\s?)(\w+)(=|=~|!=|!~)(?:\"([^"=~!]+)\"|([^"=~!]+)|\"\")`)
|
re = regexp.MustCompile(
|
||||||
|
// '=~' has to come before '=' because otherwise only the '='
|
||||||
|
// will be consumed, and the '~' will be part of the 3rd token.
|
||||||
|
`^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(=~|=|!=|!~)\s*((?s).*?)\s*$`,
|
||||||
|
)
|
||||||
typeMap = map[string]MatchType{
|
typeMap = map[string]MatchType{
|
||||||
"=": MatchEqual,
|
"=": MatchEqual,
|
||||||
"!=": MatchNotEqual,
|
"!=": MatchNotEqual,
|
||||||
|
@ -98,31 +103,55 @@ func ParseMatchers(s string) ([]*Matcher, error) {
|
||||||
// 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, error) {
|
||||||
var (
|
|
||||||
name, value string
|
|
||||||
matchType MatchType
|
|
||||||
)
|
|
||||||
|
|
||||||
ms := re.FindStringSubmatch(s)
|
ms := re.FindStringSubmatch(s)
|
||||||
if len(ms) < 4 {
|
if len(ms) == 0 {
|
||||||
return nil, errors.Errorf("bad matcher format: %s", s)
|
return nil, errors.Errorf("bad matcher format: %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
name = ms[1]
|
var (
|
||||||
if name == "" {
|
rawValue = strings.TrimPrefix(ms[3], "\"")
|
||||||
return nil, errors.New("failed to parse label name")
|
value strings.Builder
|
||||||
|
escaped bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if !utf8.ValidString(rawValue) {
|
||||||
|
return nil, errors.Errorf("matcher value not valid UTF-8: %s", rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
matchType, found := typeMap[ms[2]]
|
// Unescape the rawValue:
|
||||||
if !found {
|
for i, r := range rawValue {
|
||||||
return nil, errors.New("failed to find match operator")
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
switch r {
|
||||||
|
case 'n':
|
||||||
|
value.WriteByte('\n')
|
||||||
|
case '"', '\\':
|
||||||
|
value.WriteRune(r)
|
||||||
|
default:
|
||||||
|
// This was a spurious escape, so treat the '\' as literal.
|
||||||
|
value.WriteByte('\\')
|
||||||
|
value.WriteRune(r)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case '\\':
|
||||||
|
if i < len(rawValue)-1 {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// '\' encountered as last byte. Treat it as literal.
|
||||||
|
value.WriteByte('\\')
|
||||||
|
case '"':
|
||||||
|
if i < len(rawValue)-1 { // Otherwise this is a trailing quote.
|
||||||
|
return nil, errors.Errorf(
|
||||||
|
"matcher value contains unescaped double quote: %s", rawValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
value.WriteRune(r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ms[3] != "" {
|
return NewMatcher(typeMap[ms[2]], ms[1], value.String())
|
||||||
value = ms[3]
|
|
||||||
} else {
|
|
||||||
value = ms[4]
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewMatcher(matchType, name, value)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue