mirror of https://github.com/tidwall/tile38.git
296 lines
7.6 KiB
Go
296 lines
7.6 KiB
Go
|
// Copyright 2013-2018 The NATS Authors
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
// Package conf supports a configuration file format used by gnatsd. It is
|
||
|
// a flexible format that combines the best of traditional
|
||
|
// configuration formats and newer styles such as JSON and YAML.
|
||
|
package conf
|
||
|
|
||
|
// The format supported is less restrictive than today's formats.
|
||
|
// Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
|
||
|
// Also supports key value assigments using '=' or ':' or whiteSpace()
|
||
|
// e.g. foo = 2, foo : 2, foo 2
|
||
|
// maps can be assigned with no key separator as well
|
||
|
// semicolons as value terminators in key/value assignments are optional
|
||
|
//
|
||
|
// see parse_test.go for more examples.
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
"unicode"
|
||
|
)
|
||
|
|
||
|
type parser struct {
|
||
|
mapping map[string]interface{}
|
||
|
lx *lexer
|
||
|
|
||
|
// The current scoped context, can be array or map
|
||
|
ctx interface{}
|
||
|
|
||
|
// stack of contexts, either map or array/slice stack
|
||
|
ctxs []interface{}
|
||
|
|
||
|
// Keys stack
|
||
|
keys []string
|
||
|
|
||
|
// The config file path, empty by default.
|
||
|
fp string
|
||
|
}
|
||
|
|
||
|
// Parse will return a map of keys to interface{}, although concrete types
|
||
|
// underly them. The values supported are string, bool, int64, float64, DateTime.
|
||
|
// Arrays and nested Maps are also supported.
|
||
|
func Parse(data string) (map[string]interface{}, error) {
|
||
|
p, err := parse(data, "")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return p.mapping, nil
|
||
|
}
|
||
|
|
||
|
// ParseFile is a helper to open file, etc. and parse the contents.
|
||
|
func ParseFile(fp string) (map[string]interface{}, error) {
|
||
|
data, err := ioutil.ReadFile(fp)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error opening config file: %v", err)
|
||
|
}
|
||
|
|
||
|
p, err := parse(string(data), filepath.Dir(fp))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return p.mapping, nil
|
||
|
}
|
||
|
|
||
|
func parse(data, fp string) (p *parser, err error) {
|
||
|
p = &parser{
|
||
|
mapping: make(map[string]interface{}),
|
||
|
lx: lex(data),
|
||
|
ctxs: make([]interface{}, 0, 4),
|
||
|
keys: make([]string, 0, 4),
|
||
|
fp: fp,
|
||
|
}
|
||
|
p.pushContext(p.mapping)
|
||
|
|
||
|
for {
|
||
|
it := p.next()
|
||
|
if it.typ == itemEOF {
|
||
|
break
|
||
|
}
|
||
|
if err := p.processItem(it); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return p, nil
|
||
|
}
|
||
|
|
||
|
func (p *parser) next() item {
|
||
|
return p.lx.nextItem()
|
||
|
}
|
||
|
|
||
|
func (p *parser) pushContext(ctx interface{}) {
|
||
|
p.ctxs = append(p.ctxs, ctx)
|
||
|
p.ctx = ctx
|
||
|
}
|
||
|
|
||
|
func (p *parser) popContext() interface{} {
|
||
|
if len(p.ctxs) == 0 {
|
||
|
panic("BUG in parser, context stack empty")
|
||
|
}
|
||
|
li := len(p.ctxs) - 1
|
||
|
last := p.ctxs[li]
|
||
|
p.ctxs = p.ctxs[0:li]
|
||
|
p.ctx = p.ctxs[len(p.ctxs)-1]
|
||
|
return last
|
||
|
}
|
||
|
|
||
|
func (p *parser) pushKey(key string) {
|
||
|
p.keys = append(p.keys, key)
|
||
|
}
|
||
|
|
||
|
func (p *parser) popKey() string {
|
||
|
if len(p.keys) == 0 {
|
||
|
panic("BUG in parser, keys stack empty")
|
||
|
}
|
||
|
li := len(p.keys) - 1
|
||
|
last := p.keys[li]
|
||
|
p.keys = p.keys[0:li]
|
||
|
return last
|
||
|
}
|
||
|
|
||
|
func (p *parser) processItem(it item) error {
|
||
|
switch it.typ {
|
||
|
case itemError:
|
||
|
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
|
||
|
case itemKey:
|
||
|
p.pushKey(it.val)
|
||
|
case itemMapStart:
|
||
|
newCtx := make(map[string]interface{})
|
||
|
p.pushContext(newCtx)
|
||
|
case itemMapEnd:
|
||
|
p.setValue(p.popContext())
|
||
|
case itemString:
|
||
|
p.setValue(it.val) // FIXME(dlc) sanitize string?
|
||
|
case itemInteger:
|
||
|
lastDigit := 0
|
||
|
for _, r := range it.val {
|
||
|
if !unicode.IsDigit(r) && r != '-' {
|
||
|
break
|
||
|
}
|
||
|
lastDigit++
|
||
|
}
|
||
|
numStr := it.val[:lastDigit]
|
||
|
num, err := strconv.ParseInt(numStr, 10, 64)
|
||
|
if err != nil {
|
||
|
if e, ok := err.(*strconv.NumError); ok &&
|
||
|
e.Err == strconv.ErrRange {
|
||
|
return fmt.Errorf("Integer '%s' is out of the range.", it.val)
|
||
|
}
|
||
|
return fmt.Errorf("Expected integer, but got '%s'.", it.val)
|
||
|
}
|
||
|
// Process a suffix
|
||
|
suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
|
||
|
switch suffix {
|
||
|
case "":
|
||
|
p.setValue(num)
|
||
|
case "k":
|
||
|
p.setValue(num * 1000)
|
||
|
case "kb":
|
||
|
p.setValue(num * 1024)
|
||
|
case "m":
|
||
|
p.setValue(num * 1000 * 1000)
|
||
|
case "mb":
|
||
|
p.setValue(num * 1024 * 1024)
|
||
|
case "g":
|
||
|
p.setValue(num * 1000 * 1000 * 1000)
|
||
|
case "gb":
|
||
|
p.setValue(num * 1024 * 1024 * 1024)
|
||
|
}
|
||
|
case itemFloat:
|
||
|
num, err := strconv.ParseFloat(it.val, 64)
|
||
|
if err != nil {
|
||
|
if e, ok := err.(*strconv.NumError); ok &&
|
||
|
e.Err == strconv.ErrRange {
|
||
|
return fmt.Errorf("Float '%s' is out of the range.", it.val)
|
||
|
}
|
||
|
return fmt.Errorf("Expected float, but got '%s'.", it.val)
|
||
|
}
|
||
|
p.setValue(num)
|
||
|
case itemBool:
|
||
|
switch strings.ToLower(it.val) {
|
||
|
case "true", "yes", "on":
|
||
|
p.setValue(true)
|
||
|
case "false", "no", "off":
|
||
|
p.setValue(false)
|
||
|
default:
|
||
|
return fmt.Errorf("Expected boolean value, but got '%s'.", it.val)
|
||
|
}
|
||
|
case itemDatetime:
|
||
|
dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf(
|
||
|
"Expected Zulu formatted DateTime, but got '%s'.", it.val)
|
||
|
}
|
||
|
p.setValue(dt)
|
||
|
case itemArrayStart:
|
||
|
var array = make([]interface{}, 0)
|
||
|
p.pushContext(array)
|
||
|
case itemArrayEnd:
|
||
|
array := p.ctx
|
||
|
p.popContext()
|
||
|
p.setValue(array)
|
||
|
case itemVariable:
|
||
|
if value, ok := p.lookupVariable(it.val); ok {
|
||
|
p.setValue(value)
|
||
|
} else {
|
||
|
return fmt.Errorf("Variable reference for '%s' on line %d can not be found.",
|
||
|
it.val, it.line)
|
||
|
}
|
||
|
case itemInclude:
|
||
|
m, err := ParseFile(filepath.Join(p.fp, it.val))
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error parsing include file '%s', %v.", it.val, err)
|
||
|
}
|
||
|
for k, v := range m {
|
||
|
p.pushKey(k)
|
||
|
p.setValue(v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Used to map an environment value into a temporary map to pass to secondary Parse call.
|
||
|
const pkey = "pk"
|
||
|
|
||
|
// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
|
||
|
const bcryptPrefix = "2a$"
|
||
|
|
||
|
// lookupVariable will lookup a variable reference. It will use block scoping on keys
|
||
|
// it has seen before, with the top level scoping being the environment variables. We
|
||
|
// ignore array contexts and only process the map contexts..
|
||
|
//
|
||
|
// Returns true for ok if it finds something, similar to map.
|
||
|
func (p *parser) lookupVariable(varReference string) (interface{}, bool) {
|
||
|
// Do special check to see if it is a raw bcrypt string.
|
||
|
if strings.HasPrefix(varReference, bcryptPrefix) {
|
||
|
return "$" + varReference, true
|
||
|
}
|
||
|
|
||
|
// Loop through contexts currently on the stack.
|
||
|
for i := len(p.ctxs) - 1; i >= 0; i -= 1 {
|
||
|
ctx := p.ctxs[i]
|
||
|
// Process if it is a map context
|
||
|
if m, ok := ctx.(map[string]interface{}); ok {
|
||
|
if v, ok := m[varReference]; ok {
|
||
|
return v, ok
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we are here, we have exhausted our context maps and still not found anything.
|
||
|
// Parse from the environment.
|
||
|
if vStr, ok := os.LookupEnv(varReference); ok {
|
||
|
// Everything we get here will be a string value, so we need to process as a parser would.
|
||
|
if vmap, err := Parse(fmt.Sprintf("%s=%s", pkey, vStr)); err == nil {
|
||
|
v, ok := vmap[pkey]
|
||
|
return v, ok
|
||
|
}
|
||
|
}
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
func (p *parser) setValue(val interface{}) {
|
||
|
// Test to see if we are on an array or a map
|
||
|
|
||
|
// Array processing
|
||
|
if ctx, ok := p.ctx.([]interface{}); ok {
|
||
|
p.ctx = append(ctx, val)
|
||
|
p.ctxs[len(p.ctxs)-1] = p.ctx
|
||
|
}
|
||
|
|
||
|
// Map processing
|
||
|
if ctx, ok := p.ctx.(map[string]interface{}); ok {
|
||
|
key := p.popKey()
|
||
|
// FIXME(dlc), make sure to error if redefining same key?
|
||
|
ctx[key] = val
|
||
|
}
|
||
|
}
|