Fix filter errors

Prevent error messages from being output to stderr.
Return illegal token when a quoted string is invalid and
capture the error.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
Derek McGowan 2020-01-08 18:07:50 -08:00
parent 0d276ece0e
commit 3af3a76026
No known key found for this signature in database
GPG Key ID: F58C5D0A4405ACDB
5 changed files with 103 additions and 20 deletions

View File

@ -272,6 +272,16 @@ func TestFilters(t *testing.T) {
input: "image~=,id?=?fbaq", input: "image~=,id?=?fbaq",
errString: `filters: parse error: [image~= >|,|< id?=?fbaq]: expected value or quoted`, errString: `filters: parse error: [image~= >|,|< id?=?fbaq]: expected value or quoted`,
}, },
{
name: "FieldQuotedLiteralNotTerminated",
input: "labels.ns/key==value",
errString: `filters: parse error: [labels.ns >|/|< key==value]: quoted literal not terminated`,
},
{
name: "ValueQuotedLiteralNotTerminated",
input: "labels.key==/value",
errString: `filters: parse error: [labels.key== >|/|< value]: quoted literal not terminated`,
},
} { } {
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
filter, err := Parse(testcase.input) filter, err := Parse(testcase.input)

View File

@ -209,6 +209,8 @@ func (p *parser) field() (string, error) {
return s, nil return s, nil
case tokenQuoted: case tokenQuoted:
return p.unquote(pos, s, false) return p.unquote(pos, s, false)
case tokenIllegal:
return "", p.mkerr(pos, p.scanner.err)
} }
return "", p.mkerr(pos, "expected field or quoted") return "", p.mkerr(pos, "expected field or quoted")
@ -228,6 +230,8 @@ func (p *parser) operator() (operator, error) {
default: default:
return 0, p.mkerr(pos, "unsupported operator %q", s) return 0, p.mkerr(pos, "unsupported operator %q", s)
} }
case tokenIllegal:
return 0, p.mkerr(pos, p.scanner.err)
} }
return 0, p.mkerr(pos, `expected an operator ("=="|"!="|"~=")`) return 0, p.mkerr(pos, `expected an operator ("=="|"!="|"~=")`)
@ -241,6 +245,8 @@ func (p *parser) value(allowAltQuotes bool) (string, error) {
return s, nil return s, nil
case tokenQuoted: case tokenQuoted:
return p.unquote(pos, s, allowAltQuotes) return p.unquote(pos, s, allowAltQuotes)
case tokenIllegal:
return "", p.mkerr(pos, p.scanner.err)
} }
return "", p.mkerr(pos, "expected value or quoted") return "", p.mkerr(pos, "expected value or quoted")

View File

@ -17,7 +17,6 @@
package filters package filters
import ( import (
"fmt"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
) )
@ -64,6 +63,7 @@ type scanner struct {
pos int pos int
ppos int // bounds the current rune in the string ppos int // bounds the current rune in the string
value bool value bool
err string
} }
func (s *scanner) init(input string) { func (s *scanner) init(input string) {
@ -82,12 +82,14 @@ func (s *scanner) next() rune {
s.ppos += w s.ppos += w
if r == utf8.RuneError { if r == utf8.RuneError {
if w > 0 { if w > 0 {
s.error("rune error")
return tokenIllegal return tokenIllegal
} }
return tokenEOF return tokenEOF
} }
if r == 0 { if r == 0 {
s.error("unexpected null")
return tokenIllegal return tokenIllegal
} }
@ -114,7 +116,9 @@ chomp:
case ch == tokenEOF: case ch == tokenEOF:
case ch == tokenIllegal: case ch == tokenIllegal:
case isQuoteRune(ch): case isQuoteRune(ch):
s.scanQuoted(ch) if !s.scanQuoted(ch) {
return pos, tokenIllegal, s.input[pos:s.ppos]
}
return pos, tokenQuoted, s.input[pos:s.ppos] return pos, tokenQuoted, s.input[pos:s.ppos]
case isSeparatorRune(ch): case isSeparatorRune(ch):
s.value = false s.value = false
@ -172,54 +176,64 @@ func (s *scanner) scanValue() {
} }
} }
func (s *scanner) scanQuoted(quote rune) { func (s *scanner) scanQuoted(quote rune) bool {
var illegal bool
ch := s.next() // read character after quote ch := s.next() // read character after quote
for ch != quote { for ch != quote {
if ch == '\n' || ch < 0 { if ch == '\n' || ch < 0 {
s.error("literal not terminated") s.error("quoted literal not terminated")
return return false
} }
if ch == '\\' { if ch == '\\' {
ch = s.scanEscape(quote) var legal bool
ch, legal = s.scanEscape(quote)
if !legal {
illegal = true
}
} else { } else {
ch = s.next() ch = s.next()
} }
} }
return !illegal
} }
func (s *scanner) scanEscape(quote rune) rune { func (s *scanner) scanEscape(quote rune) (ch rune, legal bool) {
ch := s.next() // read character after '/' ch = s.next() // read character after '/'
switch ch { switch ch {
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote: case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
// nothing to do // nothing to do
ch = s.next() ch = s.next()
legal = true
case '0', '1', '2', '3', '4', '5', '6', '7': case '0', '1', '2', '3', '4', '5', '6', '7':
ch = s.scanDigits(ch, 8, 3) ch, legal = s.scanDigits(ch, 8, 3)
case 'x': case 'x':
ch = s.scanDigits(s.next(), 16, 2) ch, legal = s.scanDigits(s.next(), 16, 2)
case 'u': case 'u':
ch = s.scanDigits(s.next(), 16, 4) ch, legal = s.scanDigits(s.next(), 16, 4)
case 'U': case 'U':
ch = s.scanDigits(s.next(), 16, 8) ch, legal = s.scanDigits(s.next(), 16, 8)
default: default:
s.error("illegal char escape") s.error("illegal escape sequence")
} }
return ch return
} }
func (s *scanner) scanDigits(ch rune, base, n int) rune { func (s *scanner) scanDigits(ch rune, base, n int) (rune, bool) {
for n > 0 && digitVal(ch) < base { for n > 0 && digitVal(ch) < base {
ch = s.next() ch = s.next()
n-- n--
} }
if n > 0 { if n > 0 {
s.error("illegal char escape") s.error("illegal numeric escape sequence")
return ch, false
} }
return ch return ch, true
} }
func (s *scanner) error(msg string) { func (s *scanner) error(msg string) {
fmt.Println("error fixme", msg) if s.err == "" {
s.err = msg
}
} }
func digitVal(ch rune) int { func digitVal(ch rune) int {

View File

@ -25,9 +25,13 @@ type tokenResult struct {
pos int pos int
token token token token
text string text string
err string
} }
func (tr tokenResult) String() string { func (tr tokenResult) String() string {
if tr.err != "" {
return fmt.Sprintf("{pos: %v, token: %v, text: %q, err: %q}", tr.pos, tr.token, tr.text, tr.err)
}
return fmt.Sprintf("{pos: %v, token: %v, text: %q}", tr.pos, tr.token, tr.text) return fmt.Sprintf("{pos: %v, token: %v, text: %q}", tr.pos, tr.token, tr.text)
} }
@ -171,7 +175,7 @@ func TestScanner(t *testing.T) {
input: "foo\x00bar", input: "foo\x00bar",
expected: []tokenResult{ expected: []tokenResult{
{pos: 0, token: tokenField, text: "foo"}, {pos: 0, token: tokenField, text: "foo"},
{pos: 3, token: tokenIllegal}, {pos: 3, token: tokenIllegal, err: "unexpected null"},
{pos: 4, token: tokenField, text: "bar"}, {pos: 4, token: tokenField, text: "bar"},
{pos: 7, token: tokenEOF}, {pos: 7, token: tokenEOF},
}, },
@ -271,6 +275,51 @@ func TestScanner(t *testing.T) {
{pos: 23, token: tokenEOF}, {pos: 23, token: tokenEOF},
}, },
}, },
{
name: "IllegalQuoted",
input: "labels.containerd.io/key==value",
expected: []tokenResult{
{pos: 0, token: tokenField, text: "labels"},
{pos: 6, token: tokenSeparator, text: "."},
{pos: 7, token: tokenField, text: "containerd"},
{pos: 17, token: tokenSeparator, text: "."},
{pos: 18, token: tokenField, text: "io"},
{pos: 20, token: tokenIllegal, text: "/key==value", err: "quoted literal not terminated"},
{pos: 31, token: tokenEOF},
},
},
{
name: "IllegalQuotedWithNewLine",
input: "labels.\"containerd.io\nkey\"==value",
expected: []tokenResult{
{pos: 0, token: tokenField, text: "labels"},
{pos: 6, token: tokenSeparator, text: "."},
{pos: 7, token: tokenIllegal, text: "\"containerd.io\n", err: "quoted literal not terminated"},
{pos: 22, token: tokenField, text: "key"},
{pos: 25, token: tokenIllegal, text: "\"==value", err: "quoted literal not terminated"},
{pos: 33, token: tokenEOF},
},
},
{
name: "IllegalEscapeSequence",
input: `labels."\g"`,
expected: []tokenResult{
{pos: 0, token: tokenField, text: "labels"},
{pos: 6, token: tokenSeparator, text: "."},
{pos: 7, token: tokenIllegal, text: `"\g"`, err: "illegal escape sequence"},
{pos: 11, token: tokenEOF},
},
},
{
name: "IllegalNumericEscapeSequence",
input: `labels."\xaz"`,
expected: []tokenResult{
{pos: 0, token: tokenField, text: "labels"},
{pos: 6, token: tokenSeparator, text: "."},
{pos: 7, token: tokenIllegal, text: `"\xaz"`, err: "illegal numeric escape sequence"},
{pos: 13, token: tokenEOF},
},
},
} { } {
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
var sc scanner var sc scanner
@ -296,6 +345,9 @@ func TestScanner(t *testing.T) {
if i >= len(testcase.expected) { if i >= len(testcase.expected) {
t.Fatalf("too many tokens parsed") t.Fatalf("too many tokens parsed")
} }
if tok == tokenIllegal {
tokv.err = sc.err
}
if tokv != testcase.expected[i] { if tokv != testcase.expected[i] {
t.Fatalf("token unexpected: %v != %v", tokv, testcase.expected[i]) t.Fatalf("token unexpected: %v != %v", tokv, testcase.expected[i])
@ -305,6 +357,7 @@ func TestScanner(t *testing.T) {
if tok == tokenEOF { if tok == tokenEOF {
break break
} }
} }
// make sure we've eof'd // make sure we've eof'd

View File

@ -115,7 +115,7 @@ func (s *service) List(req *api.ListContentRequest, session api.Content_ListServ
return nil return nil
}, req.Filters...); err != nil { }, req.Filters...); err != nil {
return err return errdefs.ToGRPC(err)
} }
if len(buffer) > 0 { if len(buffer) > 0 {