Add simple parsing for key/value files
This commit is contained in:
parent
25edf46db3
commit
aba688f2d2
218
key-value/key-value.go
Normal file
218
key-value/key-value.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Package keyValue implements the basic key/value file format as described in
|
||||||
|
// desktop-entry-spec version 1.5. At the moment, these files can only be read
|
||||||
|
// from and not written to.
|
||||||
|
// The specification can be read at:
|
||||||
|
// https://specifications.freedesktop.org/desktop-entry-spec/1.5/
|
||||||
|
package keyValue
|
||||||
|
|
||||||
|
// This implementation does not support writing files, because, and I quote:
|
||||||
|
// Lines beginning with a # and blank lines are considered comments and will
|
||||||
|
// be ignored, however they should be preserved across reads and writes of the
|
||||||
|
// desktop entry file.
|
||||||
|
// MISS ME WITH THAT SHIT!
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
import "bufio"
|
||||||
|
import "strings"
|
||||||
|
import "git.tebibyte.media/tomo/xdg/locale"
|
||||||
|
|
||||||
|
type keyValueError string
|
||||||
|
func (err keyValueError) Error () string { return string(err) }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrUnexpectedRune indicates an unexpected rune was encountered.
|
||||||
|
ErrUnexpectedRune = keyValueError("unexpected rune")
|
||||||
|
// ErrInvalidGroupHeader indicates a wrongly formatted group header was
|
||||||
|
// encountered.
|
||||||
|
ErrInvalidGroupHeader = keyValueError("invalid group header")
|
||||||
|
// ErrInvalidEntry indicates a wrongly formatted key/value entry was
|
||||||
|
// encountered.
|
||||||
|
ErrInvalidEntry = keyValueError("invalid entry")
|
||||||
|
// ErrDuplicateGroup indicates that two or more groups with the same
|
||||||
|
// name were found.
|
||||||
|
ErrDuplicateGroup = keyValueError("duplicate group name")
|
||||||
|
// ErrDuplicateEntry indicates that two or more keys with the same name
|
||||||
|
// were found.
|
||||||
|
ErrDuplicateEntry = keyValueError("duplicate entry name")
|
||||||
|
// ErrDuplicateLocalization indicates that two or more localized values
|
||||||
|
// with the same locale were provided for the same entry.
|
||||||
|
ErrDuplicateLocalization = keyValueError("duplicate localization")
|
||||||
|
// ErrNoDefaultValue indicates that localized values were provided for
|
||||||
|
// an entry with no default value.
|
||||||
|
ErrNoDefaultValue = keyValueError("no default value")
|
||||||
|
// ErrEntryOutsideGroup indicates that an entry was found.
|
||||||
|
ErrEntryOutsideGroup = keyValueError("entry outside group")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse parses a key/value file from a Reader.
|
||||||
|
func Parse (reader io.Reader) (File, error) {
|
||||||
|
buffer := bufio.NewReader(reader)
|
||||||
|
var file = File { }
|
||||||
|
var group Group
|
||||||
|
var specifiedValues map[string] struct { }
|
||||||
|
for {
|
||||||
|
line, err := buffer.ReadString('\n')
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// comment
|
||||||
|
case line == "", strings.HasPrefix(line, "#"):
|
||||||
|
|
||||||
|
// group header
|
||||||
|
case strings.HasPrefix(line, "["):
|
||||||
|
// check integrity of prev. group
|
||||||
|
if group != nil {
|
||||||
|
for key := range group {
|
||||||
|
if _, ok := specifiedValues[key]; !ok {
|
||||||
|
return nil, ErrNoDefaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new group
|
||||||
|
name, err := parseGroupHeader(line)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
group = Group { }
|
||||||
|
specifiedValues = map[string] struct { } { }
|
||||||
|
_, exists := file[name]
|
||||||
|
if exists { return nil, ErrDuplicateGroup }
|
||||||
|
file[name] = group
|
||||||
|
|
||||||
|
// key/value pair
|
||||||
|
default:
|
||||||
|
if group == nil { return nil, ErrEntryOutsideGroup }
|
||||||
|
key, value, loc, err := parseEntry(line)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
if loc != (locale.Locale { }) {
|
||||||
|
// localized value
|
||||||
|
entry, ok := group[key]
|
||||||
|
if !ok { entry = newEntry() }
|
||||||
|
entry.Localized[loc] = value
|
||||||
|
group[key] = entry
|
||||||
|
} else {
|
||||||
|
// default value
|
||||||
|
_, exists := group[key]
|
||||||
|
if exists { return nil, ErrDuplicateEntry }
|
||||||
|
entry := newEntry()
|
||||||
|
entry.Value = value
|
||||||
|
group[key] = entry
|
||||||
|
specifiedValues[key] = struct { } { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEntry () Entry {
|
||||||
|
return Entry {
|
||||||
|
Localized: map[locale.Locale] string { },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGroupHeader (line string) (string, error) {
|
||||||
|
line = strings.TrimPrefix(line, "[")
|
||||||
|
if !strings.HasSuffix(line, "]") {
|
||||||
|
return "", ErrInvalidGroupHeader
|
||||||
|
}
|
||||||
|
line = strings.TrimSuffix(line, "]")
|
||||||
|
if strings.ContainsAny(line, "[]") {
|
||||||
|
return "", ErrInvalidGroupHeader
|
||||||
|
}
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEntry (line string) (key, val string, loc locale.Locale, err error) {
|
||||||
|
keyb := strings.Builder { }
|
||||||
|
locb := strings.Builder { }
|
||||||
|
valb := strings.Builder { }
|
||||||
|
|
||||||
|
state := 0
|
||||||
|
const getKey = 0
|
||||||
|
const getLoc = 1
|
||||||
|
const waitVal = 2
|
||||||
|
const getVal = 3
|
||||||
|
|
||||||
|
for _, char := range line {
|
||||||
|
switch state {
|
||||||
|
case getKey:
|
||||||
|
switch char {
|
||||||
|
case '[': state = getLoc
|
||||||
|
case '=': state = getVal
|
||||||
|
default: keyb.WriteRune(char)
|
||||||
|
}
|
||||||
|
case waitVal:
|
||||||
|
if char == '=' { state = getVal }
|
||||||
|
case getLoc:
|
||||||
|
switch char {
|
||||||
|
case ']': state = waitVal
|
||||||
|
default: locb.WriteRune(char)
|
||||||
|
}
|
||||||
|
case getVal:
|
||||||
|
valb.WriteRune(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state != getVal { err = ErrInvalidEntry; return }
|
||||||
|
|
||||||
|
loc, err = locale.Parse(locb.String())
|
||||||
|
if err != nil { return}
|
||||||
|
key = strings.TrimSpace(keyb.String())
|
||||||
|
val = strings.TrimSpace(valb.String())
|
||||||
|
|
||||||
|
if !isKeyValid(key) { err = ErrInvalidEntry; return }
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyValid (key string) bool {
|
||||||
|
for _, char := range key {
|
||||||
|
if char == '-' { continue }
|
||||||
|
if 'A' <= char && char <= 'Z' { continue }
|
||||||
|
if 'a' <= char && char <= 'z' { continue }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// File represents a key/value file.
|
||||||
|
type File map[string] Group
|
||||||
|
|
||||||
|
// Group represents a group of entries.
|
||||||
|
type Group map[string] Entry
|
||||||
|
|
||||||
|
// Entry represents an entry in a group.
|
||||||
|
type Entry struct {
|
||||||
|
Value string
|
||||||
|
Localized map[locale.Locale] string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localize returns a localized value matching the specified locale, which
|
||||||
|
// should be LC_MESSAGES for a system-localized value.
|
||||||
|
//
|
||||||
|
// The matching is done as follows. If locale is of the form
|
||||||
|
// lang_COUNTRY.ENCODING@MODIFIER, then it will match a key of the form
|
||||||
|
// lang_COUNTRY@MODIFIER. If such a key does not exist, it will attempt to match
|
||||||
|
// lang_COUNTRY followed by lang@MODIFIER. Then, a match against lang by itself
|
||||||
|
// will be attempted. Finally, if no matching key is found the required key
|
||||||
|
// without a locale specified is used. The encoding from the locale value is
|
||||||
|
// ignored when matching.
|
||||||
|
func (entry Entry) Localize (locale locale.Locale) string {
|
||||||
|
withoutEncoding := locale
|
||||||
|
withoutEncoding.Encoding = ""
|
||||||
|
value, ok := entry.Localized[withoutEncoding]
|
||||||
|
if ok { return value }
|
||||||
|
|
||||||
|
withoutModifier := withoutEncoding
|
||||||
|
withoutModifier.Modifier = ""
|
||||||
|
value, ok = entry.Localized[withoutModifier]
|
||||||
|
if ok { return value }
|
||||||
|
|
||||||
|
withoutCountry := withoutEncoding
|
||||||
|
withoutCountry.Country = ""
|
||||||
|
value, ok = entry.Localized[withoutCountry]
|
||||||
|
if ok { return value }
|
||||||
|
|
||||||
|
return entry.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO have functions to parse/validate all data types
|
Loading…
Reference in New Issue
Block a user