Initial QSfera import
This commit is contained in:
+514
@@ -0,0 +1,514 @@
|
||||
// Copyright 2013-2025 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 assignments 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 (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const _EMPTY_ = ""
|
||||
|
||||
type parser struct {
|
||||
mapping map[string]any
|
||||
lx *lexer
|
||||
|
||||
// The current scoped context, can be array or map
|
||||
ctx any
|
||||
|
||||
// stack of contexts, either map or array/slice stack
|
||||
ctxs []any
|
||||
|
||||
// Keys stack
|
||||
keys []string
|
||||
|
||||
// Keys stack as items
|
||||
ikeys []item
|
||||
|
||||
// The config file path, empty by default.
|
||||
fp string
|
||||
|
||||
// pedantic reports error when configuration is not correct.
|
||||
pedantic bool
|
||||
|
||||
// Tracks environment variable references, to avoid cycles
|
||||
envVarReferences map[string]bool
|
||||
}
|
||||
|
||||
// Parse will return a map of keys to any, 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]any, error) {
|
||||
p, err := parse(data, "", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.mapping, nil
|
||||
}
|
||||
|
||||
// ParseWithChecks is equivalent to Parse but runs in pedantic mode.
|
||||
func ParseWithChecks(data string) (map[string]any, error) {
|
||||
p, err := parse(data, "", true)
|
||||
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]any, error) {
|
||||
data, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
|
||||
p, err := parse(string(data), fp, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.mapping, nil
|
||||
}
|
||||
|
||||
// ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
|
||||
func ParseFileWithChecks(fp string) (map[string]any, error) {
|
||||
data, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err := parse(string(data), fp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.mapping, nil
|
||||
}
|
||||
|
||||
// configDigest returns a digest for the parsed config.
|
||||
func configDigest(m map[string]any) (string, error) {
|
||||
digest := sha256.New()
|
||||
e := json.NewEncoder(digest)
|
||||
if err := e.Encode(m); err != nil {
|
||||
return _EMPTY_, err
|
||||
}
|
||||
return fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// ParseFileWithChecksDigest returns the processed config and a digest
|
||||
// that represents the configuration.
|
||||
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
|
||||
m, err := ParseFileWithChecks(fp)
|
||||
if err != nil {
|
||||
return nil, _EMPTY_, err
|
||||
}
|
||||
digest, err := configDigest(m)
|
||||
if err != nil {
|
||||
return nil, _EMPTY_, err
|
||||
}
|
||||
return m, digest, nil
|
||||
}
|
||||
|
||||
type token struct {
|
||||
item item
|
||||
value any
|
||||
usedVariable bool
|
||||
sourceFile string
|
||||
}
|
||||
|
||||
func (t *token) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.value)
|
||||
}
|
||||
|
||||
func (t *token) Value() any {
|
||||
return t.value
|
||||
}
|
||||
|
||||
func (t *token) Line() int {
|
||||
return t.item.line
|
||||
}
|
||||
|
||||
func (t *token) IsUsedVariable() bool {
|
||||
return t.usedVariable
|
||||
}
|
||||
|
||||
func (t *token) SourceFile() string {
|
||||
return t.sourceFile
|
||||
}
|
||||
|
||||
func (t *token) Position() int {
|
||||
return t.item.pos
|
||||
}
|
||||
|
||||
func newParser(data, fp string, pedantic bool) *parser {
|
||||
return &parser{
|
||||
mapping: make(map[string]any),
|
||||
lx: lex(data),
|
||||
ctxs: make([]any, 0, 4),
|
||||
keys: make([]string, 0, 4),
|
||||
ikeys: make([]item, 0, 4),
|
||||
fp: filepath.Dir(fp),
|
||||
pedantic: pedantic,
|
||||
envVarReferences: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func parse(data, fp string, pedantic bool) (*parser, error) {
|
||||
p := newParser(data, fp, pedantic)
|
||||
if err := p.parse(fp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func parseEnv(data string, parent *parser) (*parser, error) {
|
||||
p := newParser(data, "", false)
|
||||
p.envVarReferences = parent.envVarReferences
|
||||
if err := p.parse(""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *parser) parse(fp string) error {
|
||||
p.pushContext(p.mapping)
|
||||
|
||||
var prevItem item
|
||||
for {
|
||||
it := p.next()
|
||||
if it.typ == itemEOF {
|
||||
// Here we allow the final character to be a bracket '}'
|
||||
// in order to support JSON like configurations.
|
||||
if prevItem.typ == itemKey && prevItem.val != mapEndString {
|
||||
return fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
|
||||
}
|
||||
break
|
||||
}
|
||||
prevItem = it
|
||||
if err := p.processItem(it, fp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) next() item {
|
||||
return p.lx.nextItem()
|
||||
}
|
||||
|
||||
func (p *parser) pushContext(ctx any) {
|
||||
p.ctxs = append(p.ctxs, ctx)
|
||||
p.ctx = ctx
|
||||
}
|
||||
|
||||
func (p *parser) popContext() any {
|
||||
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) pushItemKey(key item) {
|
||||
p.ikeys = append(p.ikeys, key)
|
||||
}
|
||||
|
||||
func (p *parser) popItemKey() item {
|
||||
if len(p.ikeys) == 0 {
|
||||
panic("BUG in parser, item keys stack empty")
|
||||
}
|
||||
li := len(p.ikeys) - 1
|
||||
last := p.ikeys[li]
|
||||
p.ikeys = p.ikeys[0:li]
|
||||
return last
|
||||
}
|
||||
|
||||
func (p *parser) processItem(it item, fp string) error {
|
||||
setValue := func(it item, v any) {
|
||||
if p.pedantic {
|
||||
p.setValue(&token{it, v, false, fp})
|
||||
} else {
|
||||
p.setValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
switch it.typ {
|
||||
case itemError:
|
||||
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
|
||||
case itemKey:
|
||||
// Keep track of the keys as items and strings,
|
||||
// we do this in order to be able to still support
|
||||
// includes without many breaking changes.
|
||||
p.pushKey(it.val)
|
||||
|
||||
if p.pedantic {
|
||||
p.pushItemKey(it)
|
||||
}
|
||||
case itemMapStart:
|
||||
newCtx := make(map[string]any)
|
||||
p.pushContext(newCtx)
|
||||
case itemMapEnd:
|
||||
setValue(it, p.popContext())
|
||||
case itemString:
|
||||
// FIXME(dlc) sanitize string?
|
||||
setValue(it, it.val)
|
||||
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 "":
|
||||
setValue(it, num)
|
||||
case "k":
|
||||
setValue(it, num*1000)
|
||||
case "kb", "ki", "kib":
|
||||
setValue(it, num*1024)
|
||||
case "m":
|
||||
setValue(it, num*1000*1000)
|
||||
case "mb", "mi", "mib":
|
||||
setValue(it, num*1024*1024)
|
||||
case "g":
|
||||
setValue(it, num*1000*1000*1000)
|
||||
case "gb", "gi", "gib":
|
||||
setValue(it, num*1024*1024*1024)
|
||||
case "t":
|
||||
setValue(it, num*1000*1000*1000*1000)
|
||||
case "tb", "ti", "tib":
|
||||
setValue(it, num*1024*1024*1024*1024)
|
||||
case "p":
|
||||
setValue(it, num*1000*1000*1000*1000*1000)
|
||||
case "pb", "pi", "pib":
|
||||
setValue(it, num*1024*1024*1024*1024*1024)
|
||||
case "e":
|
||||
setValue(it, num*1000*1000*1000*1000*1000*1000)
|
||||
case "eb", "ei", "eib":
|
||||
setValue(it, num*1024*1024*1024*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)
|
||||
}
|
||||
setValue(it, num)
|
||||
case itemBool:
|
||||
switch strings.ToLower(it.val) {
|
||||
case "true", "yes", "on":
|
||||
setValue(it, true)
|
||||
case "false", "no", "off":
|
||||
setValue(it, 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)
|
||||
}
|
||||
setValue(it, dt)
|
||||
case itemArrayStart:
|
||||
var array = make([]any, 0)
|
||||
p.pushContext(array)
|
||||
case itemArrayEnd:
|
||||
array := p.ctx
|
||||
p.popContext()
|
||||
setValue(it, array)
|
||||
case itemVariable:
|
||||
value, found, err := p.lookupVariable(it.val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
|
||||
it.val, it.line, err)
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("variable reference for '%s' on line %d can not be found",
|
||||
it.val, it.line)
|
||||
}
|
||||
|
||||
if p.pedantic {
|
||||
switch tk := value.(type) {
|
||||
case *token:
|
||||
// Mark the looked up variable as used, and make
|
||||
// the variable reference become handled as a token.
|
||||
tk.usedVariable = true
|
||||
p.setValue(&token{it, tk.Value(), false, fp})
|
||||
default:
|
||||
// Special case to add position context to bcrypt references.
|
||||
p.setValue(&token{it, value, false, fp})
|
||||
}
|
||||
} else {
|
||||
p.setValue(value)
|
||||
}
|
||||
case itemInclude:
|
||||
var (
|
||||
m map[string]any
|
||||
err error
|
||||
)
|
||||
if p.pedantic {
|
||||
m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
|
||||
} else {
|
||||
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)
|
||||
|
||||
if p.pedantic {
|
||||
switch tk := v.(type) {
|
||||
case *token:
|
||||
p.pushItemKey(tk.item)
|
||||
}
|
||||
}
|
||||
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) (any, bool, error) {
|
||||
// Do special check to see if it is a raw bcrypt string.
|
||||
if strings.HasPrefix(varReference, bcryptPrefix) {
|
||||
return "$" + varReference, true, nil
|
||||
}
|
||||
|
||||
// Loop through contexts currently on the stack.
|
||||
for i := len(p.ctxs) - 1; i >= 0; i-- {
|
||||
ctx := p.ctxs[i]
|
||||
// Process if it is a map context
|
||||
if m, ok := ctx.(map[string]any); ok {
|
||||
if v, ok := m[varReference]; ok {
|
||||
return v, ok, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are here, we have exhausted our context maps and still not found anything.
|
||||
// Detect reference cycles
|
||||
if p.envVarReferences[varReference] {
|
||||
return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
|
||||
}
|
||||
p.envVarReferences[varReference] = true
|
||||
defer delete(p.envVarReferences, varReference)
|
||||
|
||||
// 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 subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
|
||||
v, ok := subp.mapping[pkey]
|
||||
return v, ok, nil
|
||||
} else {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (p *parser) setValue(val any) {
|
||||
// Test to see if we are on an array or a map
|
||||
|
||||
// Array processing
|
||||
if ctx, ok := p.ctx.([]any); ok {
|
||||
p.ctx = append(ctx, val)
|
||||
p.ctxs[len(p.ctxs)-1] = p.ctx
|
||||
}
|
||||
|
||||
// Map processing
|
||||
if ctx, ok := p.ctx.(map[string]any); ok {
|
||||
key := p.popKey()
|
||||
|
||||
if p.pedantic {
|
||||
// Change the position to the beginning of the key
|
||||
// since more useful when reporting errors.
|
||||
switch v := val.(type) {
|
||||
case *token:
|
||||
it := p.popItemKey()
|
||||
v.item.pos = it.pos
|
||||
v.item.line = it.line
|
||||
ctx[key] = v
|
||||
}
|
||||
} else {
|
||||
// FIXME(dlc), make sure to error if redefining same key?
|
||||
ctx[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user