diff --git a/key-value/key-value.go b/key-value/key-value.go index 3616eba..b234ab8 100644 --- a/key-value/key-value.go +++ b/key-value/key-value.go @@ -14,6 +14,8 @@ package keyValue import "io" import "bufio" import "strings" +import "strconv" +import "unicode" import "git.tebibyte.media/tomo/xdg/locale" type keyValueError string @@ -42,6 +44,16 @@ const ( ErrNoDefaultValue = keyValueError("no default value") // ErrEntryOutsideGroup indicates that an entry was found. ErrEntryOutsideGroup = keyValueError("entry outside group") + // ErrUnsupportedEscape indicates that an unsupported escape sequence + // was found. + ErrUnsupportedEscape = keyValueError("unsupported escape") + + // ErrStringNotASCII indicates that a string or iconstring value was not + // found to contain valid ASCII text. + ErrStringNotASCII = keyValueError("string not ascii") + // ErrBooleanNotTrueOrFalse indicates that a boolean value was not found + // to equal "true" or "false". + ErrBooleanNotTrueOrFalse = keyValueError("boolean not true or false") ) // Parse parses a key/value file from a Reader. @@ -216,3 +228,133 @@ func (entry Entry) Localize (locale locale.Locale) string { } // TODO have functions to parse/validate all data types + +// ParseString parses a value of type string. +// Values of type string may contain all ASCII characters except for control +// characters. +// The escape sequences \s, \n, \t, \r, and \\ are supported, meaning ASCII +// space, newline, tab, carriage return, and backslash, respectively. +func ParseString (value string) (string, error) { + value, err := escapeString(value) + if err != nil { return "", err } + if !isAsciiText(value) { return "", ErrStringNotASCII } + return value, nil +} + +// ParseLocaleString parses a value of type localestring. +// Values of type localestring are user displayable, and are encoded in UTF-8. +// The escape sequences \s, \n, \t, \r, and \\ are supported, meaning ASCII +// space, newline, tab, carriage return, and backslash, respectively. +func ParseLocaleString (value string) (string, error) { + return value, nil +} + +// ParseIconString parses a value of type iconstring. +// Values of type iconstring are the names of icons; these may be absolute +// paths, or symbolic names for icons located using the algorithm described in +// the Icon Theme Specification. Such values are not user-displayable, and are +// encoded in UTF-8. +// The escape sequences \s, \n, \t, \r, and \\ are supported, meaning ASCII +// space, newline, tab, carriage return, and backslash, respectively. +func ParseIconString (value string) (string, error) { + value, err := escapeString(value) + if err != nil { return "", err } + if !isAsciiText(value) { return "", ErrStringNotASCII } + return value, nil +} + +// ParseBoolean parses a value of type boolean. +// Values of type boolean must either be the string true or false. +func ParseBoolean (value string) (bool, error) { + if value == "true" { return true, nil } + if value == "false" { return false, nil } + return false, ErrBooleanNotTrueOrFalse +} + +// ParseNumeric parses a value of type numeric. +// Values of type numeric must be a valid floating point number as recognized by +// the %f specifier for scanf in the C locale. +func ParseNumeric (value string) (float64, error) { + // TODO ensure this is compliant + return strconv.ParseFloat(value, 64) +} + +// ParseMultiple parses multiple of a value type. Any value parsing function can +// be specified. The multiple values should be separated by a semicolon and the +// input string may be optionally terminated by a semicolon. Trailing empty +// strings must always be terminated with a semicolon. Semicolons in these +// values need to be escaped using \;. +func ParseMultiple[T any] (parser func (string) (T, error), value string) ([]T, error) { + values := []T { } + builder := strings.Builder { } + + newValue := func () error { + value, err := parser(builder.String()) + if err != nil { return err } + values = append(values, value) + builder.Reset() + return nil + } + + backslash := false + for _, char := range value { + switch char { + case '\\': + backslash = true + case ';': + if backslash { + builder.WriteRune(';') + backslash = false + } else { + err := newValue() + if err != nil { return nil, err } + } + default: + if backslash { + builder.WriteRune('\\') + backslash = false + } + builder.WriteRune(char) + } + } + if backslash { + builder.WriteRune('\\') + } + if builder.Len() > 0 { + err := newValue() + if err != nil { return nil, err } + } + return values, nil +} + +func isAsciiText (value string) bool { + for _, char := range value { + // must be an ascii character that isn't a control character. + if char <= 0x1F || char > unicode.MaxASCII { + return false + } + } + return true +} + +func escapeString (value string) (string, error) { + builder := strings.Builder { } + backslash := false + for _, char := range value { + if char == '\\' { backslash = true; continue } + + if backslash { + switch char { + case 's': builder.WriteRune(' ') + case 'n': builder.WriteRune('\n') + case 't': builder.WriteRune('\t') + case 'r': builder.WriteRune('\r') + case '\\': builder.WriteRune('\\') + default: return "", ErrUnsupportedEscape + } + } else { + builder.WriteRune(char) + } + } + return builder.String(), nil +}