3d4fa7f65d
This patch updates the go-discover library to use the new config parser which uses a different encoding scheme for the go-discover config DSL. Values are no longer URL encoded but taken literally unless they contain spaces, backslashes or double quotes. To support keys or values with these special characters the string needs to be double quoted and usual escaping rules apply. Fixes #3417
227 lines
4.6 KiB
Go
227 lines
4.6 KiB
Go
package discover
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Config stores key/value pairs for the discovery
|
|
// functions to use.
|
|
type Config map[string]string
|
|
|
|
// Parse parses a "key=val key=val ..." string into a config map. Keys
|
|
// and values which contain spaces, backslashes or double-quotes must be
|
|
// quoted with double quotes. Use the backslash to escape special
|
|
// characters within quoted strings, e.g. "some key"="some \"value\"".
|
|
func Parse(s string) (Config, error) {
|
|
return parse(s)
|
|
}
|
|
|
|
// String formats a config map into the "key=val key=val ..."
|
|
// understood by Parse. The order of the keys is stable.
|
|
func (c Config) String() string {
|
|
// sort 'provider' to the front and keep the keys stable.
|
|
var keys []string
|
|
for k := range c {
|
|
if k != "provider" {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
keys = append([]string{"provider"}, keys...)
|
|
|
|
quote := func(s string) string {
|
|
if strings.ContainsAny(s, ` "\`) {
|
|
return strconv.Quote(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
var vals []string
|
|
for _, k := range keys {
|
|
v := c[k]
|
|
if v == "" {
|
|
continue
|
|
}
|
|
vals = append(vals, quote(k)+"="+quote(v))
|
|
}
|
|
return strings.Join(vals, " ")
|
|
}
|
|
|
|
func parse(in string) (Config, error) {
|
|
m := Config{}
|
|
s := []rune(strings.TrimSpace(in))
|
|
state := stateKey
|
|
key := ""
|
|
for {
|
|
// exit condition
|
|
if len(s) == 0 {
|
|
break
|
|
}
|
|
|
|
// get the next token
|
|
item, val, n := lex(s)
|
|
s = s[n:]
|
|
// fmt.Printf("parse: state: %q item: %q val: '%s' n: %d rest: '%s'\n", state, item, val, n, string(s))
|
|
|
|
switch state {
|
|
|
|
case stateKey:
|
|
switch item {
|
|
case itemText:
|
|
key = val
|
|
if _, exists := m[key]; exists {
|
|
return nil, fmt.Errorf("%s: duplicate key", key)
|
|
}
|
|
state = stateEqual
|
|
default:
|
|
return nil, fmt.Errorf("%s: %s", key, val)
|
|
}
|
|
|
|
case stateEqual:
|
|
switch item {
|
|
case itemEqual:
|
|
state = stateVal
|
|
default:
|
|
return nil, fmt.Errorf("%s: missing '='", key)
|
|
}
|
|
|
|
case stateVal:
|
|
switch item {
|
|
case itemText:
|
|
m[key] = val
|
|
state = stateKey
|
|
case itemError:
|
|
return nil, fmt.Errorf("%s: %s", key, val)
|
|
default:
|
|
return nil, fmt.Errorf("%s: missing value", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
//fmt.Printf("parse: state: %q rest: '%s'\n", state, string(s))
|
|
switch state {
|
|
case stateEqual:
|
|
return nil, fmt.Errorf("%s: missing '='", key)
|
|
case stateVal:
|
|
return nil, fmt.Errorf("%s: missing value", key)
|
|
}
|
|
if len(m) == 0 {
|
|
return nil, nil
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
type itemType string
|
|
|
|
const (
|
|
itemText itemType = "TEXT"
|
|
itemEqual = "EQUAL"
|
|
itemError = "ERROR"
|
|
)
|
|
|
|
func (t itemType) String() string {
|
|
return string(t)
|
|
}
|
|
|
|
type state string
|
|
|
|
const (
|
|
|
|
// lexer states
|
|
stateStart state = "start"
|
|
stateEqual = "equal"
|
|
stateText = "text"
|
|
stateQText = "qtext"
|
|
stateQTextEnd = "qtextend"
|
|
stateQTextEsc = "qtextesc"
|
|
|
|
// parser states
|
|
stateKey = "key"
|
|
stateVal = "val"
|
|
)
|
|
|
|
func lex(s []rune) (itemType, string, int) {
|
|
isEqual := func(r rune) bool { return r == '=' }
|
|
isEscape := func(r rune) bool { return r == '\\' }
|
|
isQuote := func(r rune) bool { return r == '"' }
|
|
isSpace := func(r rune) bool { return r == ' ' }
|
|
|
|
unquote := func(r []rune) (string, error) {
|
|
v := strings.TrimSpace(string(r))
|
|
return strconv.Unquote(v)
|
|
}
|
|
|
|
var quote rune
|
|
state := stateStart
|
|
for i, r := range s {
|
|
// fmt.Println("lex:", "i:", i, "r:", string(r), "state:", string(state), "head:", string(s[:i]), "tail:", string(s[i:]))
|
|
switch state {
|
|
case stateStart:
|
|
switch {
|
|
case isSpace(r):
|
|
// state = stateStart
|
|
case isEqual(r):
|
|
state = stateEqual
|
|
case isQuote(r):
|
|
quote = r
|
|
state = stateQText
|
|
default:
|
|
state = stateText
|
|
}
|
|
|
|
case stateEqual:
|
|
return itemEqual, "", i
|
|
|
|
case stateText:
|
|
switch {
|
|
case isEqual(r) || isSpace(r):
|
|
v := strings.TrimSpace(string(s[:i]))
|
|
return itemText, v, i
|
|
default:
|
|
// state = stateText
|
|
}
|
|
|
|
case stateQText:
|
|
switch {
|
|
case r == quote:
|
|
state = stateQTextEnd
|
|
case isEscape(r):
|
|
state = stateQTextEsc
|
|
default:
|
|
// state = stateQText
|
|
}
|
|
|
|
case stateQTextEsc:
|
|
state = stateQText
|
|
|
|
case stateQTextEnd:
|
|
v, err := unquote(s[:i])
|
|
if err != nil {
|
|
return itemError, err.Error(), i
|
|
}
|
|
return itemText, v, i
|
|
}
|
|
}
|
|
|
|
// fmt.Println("lex:", "state:", string(state))
|
|
switch state {
|
|
case stateEqual:
|
|
return itemEqual, "", len(s)
|
|
case stateQText:
|
|
return itemError, "unbalanced quotes", len(s)
|
|
case stateQTextEsc:
|
|
return itemError, "unterminated escape sequence", len(s)
|
|
case stateQTextEnd:
|
|
v, err := unquote(s)
|
|
if err != nil {
|
|
return itemError, err.Error(), len(s)
|
|
}
|
|
return itemText, v, len(s)
|
|
default:
|
|
return itemText, strings.TrimSpace(string(s)), len(s)
|
|
}
|
|
}
|