get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/conf/parse.go (about)

     1  // Copyright 2013-2018 The NATS Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package conf supports a configuration file format used by gnatsd. It is
    15  // a flexible format that combines the best of traditional
    16  // configuration formats and newer styles such as JSON and YAML.
    17  package conf
    18  
    19  // The format supported is less restrictive than today's formats.
    20  // Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
    21  // Also supports key value assignments using '=' or ':' or whiteSpace()
    22  //   e.g. foo = 2, foo : 2, foo 2
    23  // maps can be assigned with no key separator as well
    24  // semicolons as value terminators in key/value assignments are optional
    25  //
    26  // see parse_test.go for more examples.
    27  
    28  import (
    29  	"fmt"
    30  	"os"
    31  	"path/filepath"
    32  	"strconv"
    33  	"strings"
    34  	"time"
    35  	"unicode"
    36  )
    37  
    38  type parser struct {
    39  	mapping map[string]interface{}
    40  	lx      *lexer
    41  
    42  	// The current scoped context, can be array or map
    43  	ctx interface{}
    44  
    45  	// stack of contexts, either map or array/slice stack
    46  	ctxs []interface{}
    47  
    48  	// Keys stack
    49  	keys []string
    50  
    51  	// Keys stack as items
    52  	ikeys []item
    53  
    54  	// The config file path, empty by default.
    55  	fp string
    56  
    57  	// pedantic reports error when configuration is not correct.
    58  	pedantic bool
    59  }
    60  
    61  // Parse will return a map of keys to interface{}, although concrete types
    62  // underly them. The values supported are string, bool, int64, float64, DateTime.
    63  // Arrays and nested Maps are also supported.
    64  func Parse(data string) (map[string]interface{}, error) {
    65  	p, err := parse(data, "", false)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return p.mapping, nil
    70  }
    71  
    72  // ParseFile is a helper to open file, etc. and parse the contents.
    73  func ParseFile(fp string) (map[string]interface{}, error) {
    74  	data, err := os.ReadFile(fp)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("error opening config file: %v", err)
    77  	}
    78  
    79  	p, err := parse(string(data), fp, false)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	return p.mapping, nil
    84  }
    85  
    86  // ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
    87  func ParseFileWithChecks(fp string) (map[string]interface{}, error) {
    88  	data, err := os.ReadFile(fp)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	p, err := parse(string(data), fp, true)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	return p.mapping, nil
    99  }
   100  
   101  type token struct {
   102  	item         item
   103  	value        interface{}
   104  	usedVariable bool
   105  	sourceFile   string
   106  }
   107  
   108  func (t *token) Value() interface{} {
   109  	return t.value
   110  }
   111  
   112  func (t *token) Line() int {
   113  	return t.item.line
   114  }
   115  
   116  func (t *token) IsUsedVariable() bool {
   117  	return t.usedVariable
   118  }
   119  
   120  func (t *token) SourceFile() string {
   121  	return t.sourceFile
   122  }
   123  
   124  func (t *token) Position() int {
   125  	return t.item.pos
   126  }
   127  
   128  func parse(data, fp string, pedantic bool) (p *parser, err error) {
   129  	p = &parser{
   130  		mapping:  make(map[string]interface{}),
   131  		lx:       lex(data),
   132  		ctxs:     make([]interface{}, 0, 4),
   133  		keys:     make([]string, 0, 4),
   134  		ikeys:    make([]item, 0, 4),
   135  		fp:       filepath.Dir(fp),
   136  		pedantic: pedantic,
   137  	}
   138  	p.pushContext(p.mapping)
   139  
   140  	var prevItem item
   141  	for {
   142  		it := p.next()
   143  		if it.typ == itemEOF {
   144  			// Here we allow the final character to be a bracket '}'
   145  			// in order to support JSON like configurations.
   146  			if prevItem.typ == itemKey && prevItem.val != mapEndString {
   147  				return nil, fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
   148  			}
   149  			break
   150  		}
   151  		prevItem = it
   152  		if err := p.processItem(it, fp); err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  	return p, nil
   157  }
   158  
   159  func (p *parser) next() item {
   160  	return p.lx.nextItem()
   161  }
   162  
   163  func (p *parser) pushContext(ctx interface{}) {
   164  	p.ctxs = append(p.ctxs, ctx)
   165  	p.ctx = ctx
   166  }
   167  
   168  func (p *parser) popContext() interface{} {
   169  	if len(p.ctxs) == 0 {
   170  		panic("BUG in parser, context stack empty")
   171  	}
   172  	li := len(p.ctxs) - 1
   173  	last := p.ctxs[li]
   174  	p.ctxs = p.ctxs[0:li]
   175  	p.ctx = p.ctxs[len(p.ctxs)-1]
   176  	return last
   177  }
   178  
   179  func (p *parser) pushKey(key string) {
   180  	p.keys = append(p.keys, key)
   181  }
   182  
   183  func (p *parser) popKey() string {
   184  	if len(p.keys) == 0 {
   185  		panic("BUG in parser, keys stack empty")
   186  	}
   187  	li := len(p.keys) - 1
   188  	last := p.keys[li]
   189  	p.keys = p.keys[0:li]
   190  	return last
   191  }
   192  
   193  func (p *parser) pushItemKey(key item) {
   194  	p.ikeys = append(p.ikeys, key)
   195  }
   196  
   197  func (p *parser) popItemKey() item {
   198  	if len(p.ikeys) == 0 {
   199  		panic("BUG in parser, item keys stack empty")
   200  	}
   201  	li := len(p.ikeys) - 1
   202  	last := p.ikeys[li]
   203  	p.ikeys = p.ikeys[0:li]
   204  	return last
   205  }
   206  
   207  func (p *parser) processItem(it item, fp string) error {
   208  	setValue := func(it item, v interface{}) {
   209  		if p.pedantic {
   210  			p.setValue(&token{it, v, false, fp})
   211  		} else {
   212  			p.setValue(v)
   213  		}
   214  	}
   215  
   216  	switch it.typ {
   217  	case itemError:
   218  		return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
   219  	case itemKey:
   220  		// Keep track of the keys as items and strings,
   221  		// we do this in order to be able to still support
   222  		// includes without many breaking changes.
   223  		p.pushKey(it.val)
   224  
   225  		if p.pedantic {
   226  			p.pushItemKey(it)
   227  		}
   228  	case itemMapStart:
   229  		newCtx := make(map[string]interface{})
   230  		p.pushContext(newCtx)
   231  	case itemMapEnd:
   232  		setValue(it, p.popContext())
   233  	case itemString:
   234  		// FIXME(dlc) sanitize string?
   235  		setValue(it, it.val)
   236  	case itemInteger:
   237  		lastDigit := 0
   238  		for _, r := range it.val {
   239  			if !unicode.IsDigit(r) && r != '-' {
   240  				break
   241  			}
   242  			lastDigit++
   243  		}
   244  		numStr := it.val[:lastDigit]
   245  		num, err := strconv.ParseInt(numStr, 10, 64)
   246  		if err != nil {
   247  			if e, ok := err.(*strconv.NumError); ok &&
   248  				e.Err == strconv.ErrRange {
   249  				return fmt.Errorf("integer '%s' is out of the range", it.val)
   250  			}
   251  			return fmt.Errorf("expected integer, but got '%s'", it.val)
   252  		}
   253  		// Process a suffix
   254  		suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
   255  
   256  		switch suffix {
   257  		case "":
   258  			setValue(it, num)
   259  		case "k":
   260  			setValue(it, num*1000)
   261  		case "kb", "ki", "kib":
   262  			setValue(it, num*1024)
   263  		case "m":
   264  			setValue(it, num*1000*1000)
   265  		case "mb", "mi", "mib":
   266  			setValue(it, num*1024*1024)
   267  		case "g":
   268  			setValue(it, num*1000*1000*1000)
   269  		case "gb", "gi", "gib":
   270  			setValue(it, num*1024*1024*1024)
   271  		case "t":
   272  			setValue(it, num*1000*1000*1000*1000)
   273  		case "tb", "ti", "tib":
   274  			setValue(it, num*1024*1024*1024*1024)
   275  		case "p":
   276  			setValue(it, num*1000*1000*1000*1000*1000)
   277  		case "pb", "pi", "pib":
   278  			setValue(it, num*1024*1024*1024*1024*1024)
   279  		case "e":
   280  			setValue(it, num*1000*1000*1000*1000*1000*1000)
   281  		case "eb", "ei", "eib":
   282  			setValue(it, num*1024*1024*1024*1024*1024*1024)
   283  		}
   284  	case itemFloat:
   285  		num, err := strconv.ParseFloat(it.val, 64)
   286  		if err != nil {
   287  			if e, ok := err.(*strconv.NumError); ok &&
   288  				e.Err == strconv.ErrRange {
   289  				return fmt.Errorf("float '%s' is out of the range", it.val)
   290  			}
   291  			return fmt.Errorf("expected float, but got '%s'", it.val)
   292  		}
   293  		setValue(it, num)
   294  	case itemBool:
   295  		switch strings.ToLower(it.val) {
   296  		case "true", "yes", "on":
   297  			setValue(it, true)
   298  		case "false", "no", "off":
   299  			setValue(it, false)
   300  		default:
   301  			return fmt.Errorf("expected boolean value, but got '%s'", it.val)
   302  		}
   303  
   304  	case itemDatetime:
   305  		dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
   306  		if err != nil {
   307  			return fmt.Errorf(
   308  				"expected Zulu formatted DateTime, but got '%s'", it.val)
   309  		}
   310  		setValue(it, dt)
   311  	case itemArrayStart:
   312  		var array = make([]interface{}, 0)
   313  		p.pushContext(array)
   314  	case itemArrayEnd:
   315  		array := p.ctx
   316  		p.popContext()
   317  		setValue(it, array)
   318  	case itemVariable:
   319  		value, found, err := p.lookupVariable(it.val)
   320  		if err != nil {
   321  			return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
   322  				it.val, it.line, err)
   323  		}
   324  		if !found {
   325  			return fmt.Errorf("variable reference for '%s' on line %d can not be found",
   326  				it.val, it.line)
   327  		}
   328  
   329  		if p.pedantic {
   330  			switch tk := value.(type) {
   331  			case *token:
   332  				// Mark the looked up variable as used, and make
   333  				// the variable reference become handled as a token.
   334  				tk.usedVariable = true
   335  				p.setValue(&token{it, tk.Value(), false, fp})
   336  			default:
   337  				// Special case to add position context to bcrypt references.
   338  				p.setValue(&token{it, value, false, fp})
   339  			}
   340  		} else {
   341  			p.setValue(value)
   342  		}
   343  	case itemInclude:
   344  		var (
   345  			m   map[string]interface{}
   346  			err error
   347  		)
   348  		if p.pedantic {
   349  			m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
   350  		} else {
   351  			m, err = ParseFile(filepath.Join(p.fp, it.val))
   352  		}
   353  		if err != nil {
   354  			return fmt.Errorf("error parsing include file '%s', %v", it.val, err)
   355  		}
   356  		for k, v := range m {
   357  			p.pushKey(k)
   358  
   359  			if p.pedantic {
   360  				switch tk := v.(type) {
   361  				case *token:
   362  					p.pushItemKey(tk.item)
   363  				}
   364  			}
   365  			p.setValue(v)
   366  		}
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  // Used to map an environment value into a temporary map to pass to secondary Parse call.
   373  const pkey = "pk"
   374  
   375  // We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
   376  const bcryptPrefix = "2a$"
   377  
   378  // lookupVariable will lookup a variable reference. It will use block scoping on keys
   379  // it has seen before, with the top level scoping being the environment variables. We
   380  // ignore array contexts and only process the map contexts..
   381  //
   382  // Returns true for ok if it finds something, similar to map.
   383  func (p *parser) lookupVariable(varReference string) (interface{}, bool, error) {
   384  	// Do special check to see if it is a raw bcrypt string.
   385  	if strings.HasPrefix(varReference, bcryptPrefix) {
   386  		return "$" + varReference, true, nil
   387  	}
   388  
   389  	// Loop through contexts currently on the stack.
   390  	for i := len(p.ctxs) - 1; i >= 0; i-- {
   391  		ctx := p.ctxs[i]
   392  		// Process if it is a map context
   393  		if m, ok := ctx.(map[string]interface{}); ok {
   394  			if v, ok := m[varReference]; ok {
   395  				return v, ok, nil
   396  			}
   397  		}
   398  	}
   399  
   400  	// If we are here, we have exhausted our context maps and still not found anything.
   401  	// Parse from the environment.
   402  	if vStr, ok := os.LookupEnv(varReference); ok {
   403  		// Everything we get here will be a string value, so we need to process as a parser would.
   404  		if vmap, err := Parse(fmt.Sprintf("%s=%s", pkey, vStr)); err == nil {
   405  			v, ok := vmap[pkey]
   406  			return v, ok, nil
   407  		} else {
   408  			return nil, false, err
   409  		}
   410  	}
   411  	return nil, false, nil
   412  }
   413  
   414  func (p *parser) setValue(val interface{}) {
   415  	// Test to see if we are on an array or a map
   416  
   417  	// Array processing
   418  	if ctx, ok := p.ctx.([]interface{}); ok {
   419  		p.ctx = append(ctx, val)
   420  		p.ctxs[len(p.ctxs)-1] = p.ctx
   421  	}
   422  
   423  	// Map processing
   424  	if ctx, ok := p.ctx.(map[string]interface{}); ok {
   425  		key := p.popKey()
   426  
   427  		if p.pedantic {
   428  			// Change the position to the beginning of the key
   429  			// since more useful when reporting errors.
   430  			switch v := val.(type) {
   431  			case *token:
   432  				it := p.popItemKey()
   433  				v.item.pos = it.pos
   434  				v.item.line = it.line
   435  				ctx[key] = v
   436  			}
   437  		} else {
   438  			// FIXME(dlc), make sure to error if redefining same key?
   439  			ctx[key] = val
   440  		}
   441  	}
   442  }