1package config
2
3import (
4	"errors"
5	"strconv"
6	"strings"
7)
8
9// parseKVSlice parses a configuration string in the form
10//
11//   key=val;key=val,key=val;key=val
12//
13// into a list of string maps. maps are separated by comma and key/value
14// pairs within a map are separated by semicolons. The first key/value
15// pair of a map can omit the key and its value will be stored under the
16// empty key. This allows support of legacy configuration formats which
17// are
18//
19//   val;opt1=val1;opt2=val2;...
20func parseKVSlice(in string) ([]map[string]string, error) {
21	var keyOrFirstVal string
22	maps := []map[string]string{}
23	m := map[string]string{}
24
25	newMap := func() {
26		if len(m) > 0 {
27			maps = append(maps, m)
28			m = map[string]string{}
29		}
30	}
31
32	v := ""
33	s := []rune(in)
34	state := stateFirstKey
35	for {
36		if len(s) == 0 {
37			break
38		}
39		typ, val, n := lex(s)
40		s = s[n:]
41		// fmt.Println("parse:", "typ:", typ, "val:", val, "v:", v, "state:", string(state), "s:", string(s))
42		switch state {
43		case stateFirstKey:
44			switch typ {
45			case itemText:
46				keyOrFirstVal = strings.TrimSpace(val)
47				state = stateAfterFirstKey
48			case itemComma, itemSemicolon:
49				continue
50			default:
51				return nil, errors.New(val)
52			}
53
54		// the first value is allowed to omit the key
55		// a=b;c=d and b;c=d are valid
56		case stateAfterFirstKey:
57			switch typ {
58			case itemEqual:
59				state = stateVal
60			case itemComma:
61				if keyOrFirstVal != "" {
62					m[""] = keyOrFirstVal
63				}
64				newMap()
65				state = stateFirstKey
66			case itemSemicolon:
67				if keyOrFirstVal != "" {
68					m[""] = keyOrFirstVal
69				}
70				state = stateKey
71			default:
72				return nil, errors.New(val)
73			}
74
75		case stateKey:
76			switch typ {
77			case itemText:
78				keyOrFirstVal = strings.TrimSpace(val)
79				state = stateEqual
80			case itemComma, itemSemicolon:
81				continue
82			default:
83				return nil, errors.New(val)
84			}
85
86		case stateEqual:
87			switch typ {
88			case itemEqual:
89				state = stateVal
90			default:
91				return nil, errors.New(val)
92			}
93
94		case stateVal:
95			switch typ {
96			case itemText, itemEqual:
97				v += val
98			case itemComma:
99				m[keyOrFirstVal] = v
100				v = ""
101				newMap()
102				state = stateFirstKey
103			case itemSemicolon:
104				m[keyOrFirstVal] = v
105				v = ""
106				state = stateKey
107			default:
108				return nil, errors.New(val)
109			}
110		}
111	}
112	switch state {
113	case stateVal:
114		m[keyOrFirstVal] = v
115	case stateAfterFirstKey:
116		if keyOrFirstVal != "" {
117			m[""] = keyOrFirstVal
118		}
119	}
120	if len(m) > 0 {
121		maps = append(maps, m)
122	}
123	if len(maps) == 0 {
124		return nil, nil
125	}
126	return maps, nil
127}
128
129type itemType string
130
131const (
132	itemText      itemType = "TEXT"
133	itemEqual              = "EQUAL"
134	itemSemicolon          = "SEMICOLON"
135	itemComma              = "COMMA"
136	itemError              = "ERROR"
137)
138
139func (t itemType) String() string {
140	return string(t)
141}
142
143type state string
144
145const (
146
147	// lexer states
148	stateStart    state = "start"
149	stateText           = "text"
150	stateQText          = "qtext"
151	stateQTextEnd       = "qtextend"
152	stateQTextEsc       = "qtextesc"
153
154	// parser states
155	stateFirstKey      = "first-key"
156	stateKey           = "key"
157	stateEqual         = "equal"
158	stateVal           = "val"
159	stateAfterFirstKey = "equal-comma-semicolon"
160)
161
162func lex(s []rune) (itemType, string, int) {
163	isComma := func(r rune) bool { return r == ',' }
164	isSemicolon := func(r rune) bool { return r == ';' }
165	isEqual := func(r rune) bool { return r == '=' }
166	isEscape := func(r rune) bool { return r == '\\' }
167	isQuote := func(r rune) bool { return r == '"' || r == '\'' }
168
169	var quote rune
170	state := stateStart
171	for i, r := range s {
172		// fmt.Println("lex:", "i:", i, "r:", string(r), "state:", string(state))
173		switch state {
174		case stateStart:
175			switch {
176			case isComma(r):
177				return itemComma, string(r), 1
178			case isSemicolon(r):
179				return itemSemicolon, string(r), 1
180			case isEqual(r):
181				return itemEqual, string(r), 1
182			case isQuote(r):
183				quote = r
184				state = stateQText
185			default:
186				state = stateText
187			}
188		case stateText:
189			switch {
190			case isComma(r) || isSemicolon(r) || isEqual(r):
191				return itemText, string(s[:i]), i
192			default:
193				// state = stateText
194			}
195		case stateQText:
196			switch {
197			case r == quote:
198				state = stateQTextEnd
199			case isEscape(r):
200				state = stateQTextEsc
201			default:
202				// state = stateQText
203			}
204
205		case stateQTextEsc:
206			state = stateQText
207
208		case stateQTextEnd:
209			v, err := strconv.Unquote(string(s[:i]))
210			if err != nil {
211				return itemError, "invalid escape sequence", i
212			}
213			return itemText, v, i
214		}
215	}
216
217	// fmt.Println("lex:", "state:", string(state))
218	switch state {
219	case stateQText:
220		return itemError, "unbalanced quotes", len(s)
221	case stateQTextEsc:
222		return itemError, "unterminated escape sequence", len(s)
223	case stateQTextEnd:
224		v, err := strconv.Unquote(string(s))
225		if err != nil {
226			return itemError, "invalid escape sequence", len(s)
227		}
228		return itemText, v, len(s)
229	default:
230		return itemText, string(s), len(s)
231	}
232}
233