mox/vendor/github.com/mjl-/sconf/parse.go
2024-06-10 18:02:47 +02:00

330 lines
6.9 KiB
Go

package sconf
import (
"bufio"
"encoding/base64"
"errors"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"time"
)
type parser struct {
prefix string // indented string
input *bufio.Reader // for reading lines at a time
line string // last read line
linenumber int
}
type parseError struct {
err error
}
func parse(path string, src io.Reader, dst interface{}) (err error) {
p := &parser{
input: bufio.NewReader(src),
}
defer func() {
x := recover()
if x == nil {
return
}
perr, ok := x.(parseError)
if ok {
err = fmt.Errorf("%s:%d: %v", path, p.linenumber, perr.err)
return
}
panic(x)
}()
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Ptr {
p.stop("destination not a pointer")
}
p.parseStruct0(v.Elem())
return
}
func (p *parser) stop(err string) {
panic(parseError{errors.New(err)})
}
func (p *parser) check(err error, action string) {
if err != nil {
p.stop(fmt.Sprintf("%s: %s", action, err))
}
}
func (p *parser) string() string {
return p.line
}
func (p *parser) leave(s string) {
p.line = s
}
func (p *parser) consume() string {
s := p.line
p.line = ""
return s
}
// Next returns whether the next line is properly indented, reading data as necessary.
func (p *parser) next() bool {
for p.line == "" {
s, err := p.input.ReadString('\n')
if s == "" {
if err == io.EOF {
return false
}
p.stop(err.Error())
}
p.linenumber++
if strings.HasPrefix(strings.TrimSpace(s), "#") {
continue
}
p.line = strings.TrimSuffix(s, "\n")
}
// Less indenting than expected. Let caller stop, returning to its caller for lower-level indent.
r := strings.HasPrefix(p.line, p.prefix)
return r
}
func (p *parser) indent() {
p.prefix += "\t"
if !p.next() {
p.stop("expected indent")
}
}
func (p *parser) unindent() {
p.prefix = p.prefix[1:]
}
var durationType = reflect.TypeOf(time.Duration(0))
func (p *parser) parseValue(v reflect.Value) reflect.Value {
t := v.Type()
if t == durationType {
s := p.consume()
d, err := time.ParseDuration(s)
p.check(err, "parsing duration")
v.Set(reflect.ValueOf(d))
return v
}
switch t.Kind() {
default:
p.stop(fmt.Sprintf("cannot parse type %v", t.Kind()))
case reflect.Bool:
s := p.consume()
switch s {
case "false":
v.SetBool(false)
case "true":
v.SetBool(true)
default:
p.stop(fmt.Sprintf("bad boolean value %q", s))
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s := p.consume()
x, err := strconv.ParseInt(s, 10, 64)
p.check(err, "parsing integer")
v.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
s := p.consume()
x, err := strconv.ParseUint(s, 10, 64)
p.check(err, "parsing integer")
v.SetUint(x)
case reflect.Float32, reflect.Float64:
s := p.consume()
x, err := strconv.ParseFloat(s, 64)
p.check(err, "parsing float")
v.SetFloat(x)
case reflect.String:
v.SetString(p.consume())
case reflect.Slice:
v = p.parseSlice(v)
case reflect.Ptr:
vv := reflect.New(t.Elem())
p.parseValue(vv.Elem())
v.Set(vv)
case reflect.Struct:
p.parseStruct(v)
case reflect.Map:
v = reflect.MakeMap(t)
p.parseMap(v)
}
return v
}
func (p *parser) parseSlice(v reflect.Value) reflect.Value {
if v.Type().Elem().Kind() == reflect.Uint8 {
s := p.consume()
buf, err := base64.StdEncoding.DecodeString(s)
p.check(err, "parsing base64")
v.SetBytes(buf)
return v
}
p.indent()
defer p.unindent()
return p.parseSlice0(v)
}
func (p *parser) parseSlice0(v reflect.Value) reflect.Value {
for p.next() {
s := p.string()
prefix := p.prefix + "-"
if !strings.HasPrefix(s, prefix) {
p.stop(fmt.Sprintf("expected item, prefix %q, saw %q", prefix, s))
}
s = s[len(prefix):]
if s != "" {
if !strings.HasPrefix(s, " ") {
p.stop("missing space after -")
}
s = s[1:]
}
p.leave(s)
vv := reflect.New(v.Type().Elem()).Elem()
vv = p.parseValue(vv)
v = reflect.Append(v, vv)
}
return v
}
func (p *parser) parseStruct(v reflect.Value) {
p.indent()
defer p.unindent()
p.parseStruct0(v)
}
func (p *parser) parseStruct0(v reflect.Value) {
seen := map[string]struct{}{}
var zeroValue reflect.Value
t := v.Type()
for p.next() {
origs := p.string()
s := origs[len(p.prefix):]
l := strings.SplitN(s, ":", 2)
if len(l) != 2 {
var more string
if strings.TrimSpace(s) == "" {
more = " (perhaps stray whitespace)"
} else if strings.HasPrefix(l[0], " ") {
more = " (perhaps mixed tab/space indenting)"
}
p.stop(fmt.Sprintf("missing colon for struct key/value on non-empty line %q%s", origs, more))
}
k := l[0]
if k == "" {
p.stop("empty key in struct")
} else if strings.HasPrefix(k, " ") {
p.stop("key in struct starting with space (perhaps mixed tab/space indenting)")
}
if _, ok := seen[k]; ok {
p.stop("duplicate key in struct")
}
seen[k] = struct{}{}
s = l[1]
if s != "" && !strings.HasPrefix(s, " ") {
p.stop("missing space after colon in struct")
}
if s != "" {
s = s[1:]
}
p.leave(s)
vv := v.FieldByName(k)
if vv == zeroValue {
var more string
if strings.TrimSpace(k) != k {
more = " (perhaps stray whitespace in key)"
}
p.stop(fmt.Sprintf("unknown key %q%s", k, more))
}
if ft, _ := t.FieldByName(k); !ft.IsExported() || isIgnore(ft.Tag.Get("sconf")) {
p.stop(fmt.Sprintf("unknown key %q (has ignore tag or not exported)", k))
}
vv.Set(p.parseValue(vv))
}
n := t.NumField()
for i := 0; i < n; i++ {
f := t.Field(i)
if !f.IsExported() || isIgnore(f.Tag.Get("sconf")) || isOptional(f.Tag.Get("sconf")) {
continue
}
if _, ok := seen[f.Name]; !ok {
p.stop(fmt.Sprintf("missing required key %q", f.Name))
}
}
}
func (p *parser) parseMap(v reflect.Value) {
p.indent()
defer p.unindent()
p.parseMap0(v)
}
func (p *parser) parseMap0(v reflect.Value) {
seen := map[string]struct{}{}
t := v.Type()
for p.next() {
origs := p.string()
s := origs[len(p.prefix):]
l := strings.SplitN(s, ":", 2)
if len(l) != 2 {
var more string
if strings.TrimSpace(s) == "" {
more = " (perhaps stray whitespace)"
} else if strings.HasPrefix(l[0], " ") {
more = " (perhaps mixed tab/space indenting)"
}
p.stop(fmt.Sprintf("missing colon for map key/value on non-empty line %q%s", origs, more))
}
k := l[0]
if k == "" {
p.stop("empty key in map")
}
if _, ok := seen[k]; ok {
p.stop("duplicate key in map")
}
seen[k] = struct{}{}
s = l[1]
if s != "" && !strings.HasPrefix(s, " ") {
var more string
if strings.HasPrefix(k, " ") {
more = " (key starts with space, perhaps mixed tab/space indenting)"
}
p.stop("missing space after colon in map" + more)
}
if s != "" {
s = s[1:]
}
vv := reflect.New(t.Elem()).Elem()
if s == "nil" {
// Special value "nil" means the zero value, no further parsing of a value.
p.leave("")
} else {
p.leave(s)
vv = p.parseValue(vv)
}
v.SetMapIndex(reflect.ValueOf(k), vv)
}
}