1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
// package ini implements an opinionated ini parser that only implements what we exactly need for WireGuard configs
// - key/values MUST live under a section
// - empty section names are NOT allowed
// - comments are indicated with a #
package ini
import (
"errors"
"fmt"
"strings"
)
// shouldSkip returns whether or not a line should be skipped, empty line (after whitespace omitting) or a comment
func shouldSkip(f string) bool {
return f == "" || strings.HasPrefix(f, "#")
}
// isSection returns whether a line is a section
// this happens when it begins with [ and ends with ]
func isSection(f string) bool {
return strings.HasPrefix(f, "[") && strings.HasSuffix(f, "]")
}
// sectionName extracts the section name from a line by removing the [ and ] prefix and suffix
func sectionName(f string) string {
name := strings.TrimSuffix(strings.TrimPrefix(f, "["), "]")
return strings.TrimSpace(name)
}
// keyValue extracts a key and a value from a line
// if no 2 components are found (separated by =), we will return an error
// the key and value have their spaces trimmed
func keyValue(f string) (string, string, error) {
sl := strings.SplitN(f, "=", 2)
if len(sl) < 2 {
return "", "", errors.New("no key/value found")
}
k := strings.TrimSpace(sl[0])
if k == "" {
return "", "", errors.New("key cannot be empty")
}
v := strings.TrimSpace(sl[1])
return k, v, nil
}
// OrderedKeys is a slice of strings that is used for an ordered map
type OrderedKeys []string
func (ok *OrderedKeys) find(name string) int {
if ok == nil {
return -1
}
for i, v := range *ok {
if v == name {
return i
}
}
return -1
}
// Remove removes a `name` from the OrderedKeys slice by finding the name
// It is a no-op if the key does not exist
func (ok *OrderedKeys) Remove(name string) {
idx := ok.find(name)
if idx == -1 {
return
}
*ok = append((*ok)[:idx], (*ok)[idx+1:]...)
}
// Section represents a single section within an ini file
// It consists of multiple key and values
type Section struct {
keyValues map[string]string
keys OrderedKeys
}
// KeyValue gets a value for key `key`
// It returns an error if the key does not exist
func (sec *Section) KeyValue(key string) (string, error) {
if v, ok := sec.keyValues[key]; ok {
return v, nil
}
return "", fmt.Errorf("key: '%s' does not exist", key)
}
func (sec *Section) newKeyValue(key string, value string) {
if sec.keyValues == nil {
sec.keyValues = make(map[string]string)
}
sec.keyValues[key] = value
sec.keys = append(sec.keys, key)
}
// AddOrReplaceKeyValue adds a key `key` with value `value`
// If the key already exists it modifies the value
func (sec *Section) AddOrReplaceKeyValue(key string, value string) {
_, err := sec.KeyValue(key)
if err == nil {
sec.keyValues[key] = value
return
}
sec.newKeyValue(key, value)
}
// AddKeyValue adds a new key `key` with value `value`
// It returns an error if the key already exists
func (sec *Section) AddKeyValue(key string, value string) error {
// get an existing key
_, err := sec.KeyValue(key)
if err == nil {
return fmt.Errorf("key: '%s' already exists", key)
}
sec.newKeyValue(key, value)
return nil
}
// RemoveKey removes a key `key` from the section
// It returns an error if the key cannot be found
func (sec *Section) RemoveKey(key string) (string, error) {
if v, ok := sec.keyValues[key]; ok {
sec.keys.Remove(key)
delete(sec.keyValues, key)
return v, nil
}
return "", fmt.Errorf("no key to remove with name: '%s'", key)
}
// INI is the struct for a ini file
type INI struct {
sections map[string]*Section
keys OrderedKeys
}
// Empty returns true if there are no sections defined in the INI
func (i *INI) Empty() bool {
return len(i.keys) == 0
}
// Section gets a section from the ini file
func (i *INI) Section(name string) (*Section, error) {
if _, ok := i.sections[name]; ok {
return i.sections[name], nil
}
return nil, fmt.Errorf("section: '%s' does not exist", name)
}
// AddSection adds a section with name `name` and returns an error if the section already exists
func (i *INI) AddSection(name string) error {
// get an existing section
_, err := i.Section(name)
if err == nil {
return errors.New("section: '%s' already exists")
}
if i.sections == nil {
i.sections = make(map[string]*Section)
}
i.sections[name] = &Section{}
i.keys = append(i.keys, name)
return nil
}
// String returns the representation of the ini as a string
func (i *INI) String() string {
var out strings.Builder
for _, s := range i.keys {
sec, err := i.Section(s)
if err != nil {
continue
}
out.WriteString(fmt.Sprintf("[%s]\n", s))
for _, k := range sec.keys {
v, err := sec.KeyValue(k)
if err != nil {
continue
}
delim := ""
if v != "" {
delim = " "
}
out.WriteString(fmt.Sprintf("%s =%s%s\n", k, delim, v))
}
}
return out.String()
}
// Parse returns a slice of sections
// we do not return a map as we want to ensure the same ordering of sections, keys and values
func Parse(f string) INI {
lines := strings.Split(f, "\n")
var secs INI
sec := ""
for _, line := range lines {
// clean the line
line = strings.TrimSpace(line)
if shouldSkip(line) {
continue
}
if isSection(line) {
name := sectionName(line)
// we do not allow sections with empty names
if name == "" {
continue
}
_ = secs.AddSection(name)
sec = name
continue
}
// no section has been parsed yet
// we will ignore the rest of the values
if sec == "" {
continue
}
// split key and value
key, value, err := keyValue(line)
if err != nil {
continue
}
csec := secs.sections[sec]
// This adds a new key and value to the section
// If it already exists it ignores it as this function would return an error
_ = csec.AddKeyValue(key, value)
}
return secs
}
|