diff options
Diffstat (limited to 'internal/wireguard/ini/ini.go')
| -rw-r--r-- | internal/wireguard/ini/ini.go | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/internal/wireguard/ini/ini.go b/internal/wireguard/ini/ini.go new file mode 100644 index 0000000..6791336 --- /dev/null +++ b/internal/wireguard/ini/ini.go @@ -0,0 +1,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 +} |
