mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
315715e90f
* core: Implement FastAbs to avoid repeated os.Getwd calls * Lint * Rename files
812 lines
24 KiB
Go
812 lines
24 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy 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 caddyfile
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
// Parse parses the input just enough to group tokens, in
|
|
// order, by server block. No further parsing is performed.
|
|
// Server blocks are returned in the order in which they appear.
|
|
// Directives that do not appear in validDirectives will cause
|
|
// an error. If you do not want to check for valid directives,
|
|
// pass in nil instead.
|
|
//
|
|
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
|
// will be replaced before parsing begins.
|
|
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
|
// unfortunately, we must copy the input because parsing must
|
|
// remain a read-only operation, but we have to expand environment
|
|
// variables before we parse, which changes the underlying array (#4422)
|
|
inputCopy := make([]byte, len(input))
|
|
copy(inputCopy, input)
|
|
|
|
tokens, err := allTokens(filename, inputCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := parser{
|
|
Dispenser: NewDispenser(tokens),
|
|
importGraph: importGraph{
|
|
nodes: make(map[string]struct{}),
|
|
edges: make(adjacency),
|
|
},
|
|
}
|
|
return p.parseAll()
|
|
}
|
|
|
|
// allTokens lexes the entire input, but does not parse it.
|
|
// It returns all the tokens from the input, unstructured
|
|
// and in order. It may mutate input as it expands env vars.
|
|
func allTokens(filename string, input []byte) ([]Token, error) {
|
|
return Tokenize(replaceEnvVars(input), filename)
|
|
}
|
|
|
|
// replaceEnvVars replaces all occurrences of environment variables.
|
|
// It mutates the underlying array and returns the updated slice.
|
|
func replaceEnvVars(input []byte) []byte {
|
|
var offset int
|
|
for {
|
|
begin := bytes.Index(input[offset:], spanOpen)
|
|
if begin < 0 {
|
|
break
|
|
}
|
|
begin += offset // make beginning relative to input, not offset
|
|
end := bytes.Index(input[begin+len(spanOpen):], spanClose)
|
|
if end < 0 {
|
|
break
|
|
}
|
|
end += begin + len(spanOpen) // make end relative to input, not begin
|
|
|
|
// get the name; if there is no name, skip it
|
|
envString := input[begin+len(spanOpen) : end]
|
|
if len(envString) == 0 {
|
|
offset = end + len(spanClose)
|
|
continue
|
|
}
|
|
|
|
// split the string into a key and an optional default
|
|
envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2)
|
|
|
|
// do a lookup for the env var, replace with the default if not found
|
|
envVarValue, found := os.LookupEnv(envParts[0])
|
|
if !found && len(envParts) == 2 {
|
|
envVarValue = envParts[1]
|
|
}
|
|
|
|
// get the value of the environment variable
|
|
// note that this causes one-level deep chaining
|
|
envVarBytes := []byte(envVarValue)
|
|
|
|
// splice in the value
|
|
input = append(input[:begin],
|
|
append(envVarBytes, input[end+len(spanClose):]...)...)
|
|
|
|
// continue at the end of the replacement
|
|
offset = begin + len(envVarBytes)
|
|
}
|
|
return input
|
|
}
|
|
|
|
type parser struct {
|
|
*Dispenser
|
|
block ServerBlock // current server block being parsed
|
|
eof bool // if we encounter a valid EOF in a hard place
|
|
definedSnippets map[string][]Token
|
|
nesting int
|
|
importGraph importGraph
|
|
}
|
|
|
|
func (p *parser) parseAll() ([]ServerBlock, error) {
|
|
var blocks []ServerBlock
|
|
|
|
for p.Next() {
|
|
err := p.parseOne()
|
|
if err != nil {
|
|
return blocks, err
|
|
}
|
|
if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 {
|
|
blocks = append(blocks, p.block)
|
|
}
|
|
if p.nesting > 0 {
|
|
return blocks, p.EOFErr()
|
|
}
|
|
}
|
|
|
|
return blocks, nil
|
|
}
|
|
|
|
func (p *parser) parseOne() error {
|
|
p.block = ServerBlock{}
|
|
return p.begin()
|
|
}
|
|
|
|
func (p *parser) begin() error {
|
|
if len(p.tokens) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := p.addresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.eof {
|
|
// this happens if the Caddyfile consists of only
|
|
// a line of addresses and nothing else
|
|
return nil
|
|
}
|
|
|
|
if ok, name := p.isNamedRoute(); ok {
|
|
// we just need a dummy leading token to ease parsing later
|
|
nameToken := p.Token()
|
|
nameToken.Text = name
|
|
|
|
// named routes only have one key, the route name
|
|
p.block.Keys = []Token{nameToken}
|
|
p.block.IsNamedRoute = true
|
|
|
|
// get all the tokens from the block, including the braces
|
|
tokens, err := p.blockTokens(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tokens = append([]Token{nameToken}, tokens...)
|
|
p.block.Segments = []Segment{tokens}
|
|
return nil
|
|
}
|
|
|
|
if ok, name := p.isSnippet(); ok {
|
|
if p.definedSnippets == nil {
|
|
p.definedSnippets = map[string][]Token{}
|
|
}
|
|
if _, found := p.definedSnippets[name]; found {
|
|
return p.Errf("redeclaration of previously declared snippet %s", name)
|
|
}
|
|
// consume all tokens til matched close brace
|
|
tokens, err := p.blockTokens(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Just as we need to track which file the token comes from, we need to
|
|
// keep track of which snippet the token comes from. This is helpful
|
|
// in tracking import cycles across files/snippets by namespacing them.
|
|
// Without this, we end up with false-positives in cycle-detection.
|
|
for k, v := range tokens {
|
|
v.snippetName = name
|
|
tokens[k] = v
|
|
}
|
|
p.definedSnippets[name] = tokens
|
|
// empty block keys so we don't save this block as a real server.
|
|
p.block.Keys = nil
|
|
return nil
|
|
}
|
|
|
|
return p.blockContents()
|
|
}
|
|
|
|
func (p *parser) addresses() error {
|
|
var expectingAnother bool
|
|
|
|
for {
|
|
value := p.Val()
|
|
token := p.Token()
|
|
|
|
// Reject request matchers if trying to define them globally
|
|
if strings.HasPrefix(value, "@") {
|
|
return p.Errf("request matchers may not be defined globally, they must be in a site block; found %s", value)
|
|
}
|
|
|
|
// Special case: import directive replaces tokens during parse-time
|
|
if value == "import" && p.isNewLine() {
|
|
err := p.doImport(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Open brace definitely indicates end of addresses
|
|
if value == "{" {
|
|
if expectingAnother {
|
|
return p.Errf("Expected another address but had '%s' - check for extra comma", value)
|
|
}
|
|
// Mark this server block as being defined with braces.
|
|
// This is used to provide a better error message when
|
|
// the user may have tried to define two server blocks
|
|
// without having used braces, which are required in
|
|
// that case.
|
|
p.block.HasBraces = true
|
|
break
|
|
}
|
|
|
|
// Users commonly forget to place a space between the address and the '{'
|
|
if strings.HasSuffix(value, "{") {
|
|
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", value)
|
|
}
|
|
|
|
if value != "" { // empty token possible if user typed ""
|
|
// Trailing comma indicates another address will follow, which
|
|
// may possibly be on the next line
|
|
if value[len(value)-1] == ',' {
|
|
value = value[:len(value)-1]
|
|
expectingAnother = true
|
|
} else {
|
|
expectingAnother = false // but we may still see another one on this line
|
|
}
|
|
|
|
// If there's a comma here, it's probably because they didn't use a space
|
|
// between their two domains, e.g. "foo.com,bar.com", which would not be
|
|
// parsed as two separate site addresses.
|
|
if strings.Contains(value, ",") {
|
|
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
|
|
}
|
|
|
|
// After the above, a comma surrounded by spaces would result
|
|
// in an empty token which we should ignore
|
|
if value != "" {
|
|
// Add the token as a site address
|
|
token.Text = value
|
|
p.block.Keys = append(p.block.Keys, token)
|
|
}
|
|
}
|
|
|
|
// Advance token and possibly break out of loop or return error
|
|
hasNext := p.Next()
|
|
if expectingAnother && !hasNext {
|
|
return p.EOFErr()
|
|
}
|
|
if !hasNext {
|
|
p.eof = true
|
|
break // EOF
|
|
}
|
|
if !expectingAnother && p.isNewLine() {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) blockContents() error {
|
|
errOpenCurlyBrace := p.openCurlyBrace()
|
|
if errOpenCurlyBrace != nil {
|
|
// single-server configs don't need curly braces
|
|
p.cursor--
|
|
}
|
|
|
|
err := p.directives()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// only look for close curly brace if there was an opening
|
|
if errOpenCurlyBrace == nil {
|
|
err = p.closeCurlyBrace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// directives parses through all the lines for directives
|
|
// and it expects the next token to be the first
|
|
// directive. It goes until EOF or closing curly brace
|
|
// which ends the server block.
|
|
func (p *parser) directives() error {
|
|
for p.Next() {
|
|
// end of server block
|
|
if p.Val() == "}" {
|
|
// p.nesting has already been decremented
|
|
break
|
|
}
|
|
|
|
// special case: import directive replaces tokens during parse-time
|
|
if p.Val() == "import" {
|
|
err := p.doImport(1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
|
continue
|
|
}
|
|
|
|
// normal case: parse a directive as a new segment
|
|
// (a "segment" is a line which starts with a directive
|
|
// and which ends at the end of the line or at the end of
|
|
// the block that is opened at the end of the line)
|
|
if err := p.directive(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// doImport swaps out the import directive and its argument
|
|
// (a total of 2 tokens) with the tokens in the specified file
|
|
// or globbing pattern. When the function returns, the cursor
|
|
// is on the token before where the import directive was. In
|
|
// other words, call Next() to access the first token that was
|
|
// imported.
|
|
func (p *parser) doImport(nesting int) error {
|
|
// syntax checks
|
|
if !p.NextArg() {
|
|
return p.ArgErr()
|
|
}
|
|
importPattern := p.Val()
|
|
if importPattern == "" {
|
|
return p.Err("Import requires a non-empty filepath")
|
|
}
|
|
|
|
// grab remaining args as placeholder replacements
|
|
args := p.RemainingArgs()
|
|
|
|
// set up a replacer for non-variadic args replacement
|
|
repl := makeArgsReplacer(args)
|
|
|
|
// grab all the tokens (if it exists) from within a block that follows the import
|
|
var blockTokens []Token
|
|
for currentNesting := p.Nesting(); p.NextBlock(currentNesting); {
|
|
blockTokens = append(blockTokens, p.Token())
|
|
}
|
|
// initialize with size 1
|
|
blockMapping := make(map[string][]Token, 1)
|
|
if len(blockTokens) > 0 {
|
|
// use such tokens to create a new dispenser, and then use it to parse each block
|
|
bd := NewDispenser(blockTokens)
|
|
for bd.Next() {
|
|
// see if we can grab a key
|
|
var currentMappingKey string
|
|
if bd.Val() == "{" {
|
|
return p.Err("anonymous blocks are not supported")
|
|
}
|
|
currentMappingKey = bd.Val()
|
|
currentMappingTokens := []Token{}
|
|
// read all args until end of line / {
|
|
if bd.NextArg() {
|
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
|
for bd.NextArg() {
|
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
|
}
|
|
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
|
|
// maybe someone can do that in the future
|
|
} else {
|
|
// attempt to enter a block and add tokens to the currentMappingTokens
|
|
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
|
}
|
|
}
|
|
blockMapping[currentMappingKey] = currentMappingTokens
|
|
}
|
|
}
|
|
|
|
// splice out the import directive and its arguments
|
|
// (2 tokens, plus the length of args)
|
|
tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)]
|
|
tokensAfter := p.tokens[p.cursor+1:]
|
|
var importedTokens []Token
|
|
var nodes []string
|
|
|
|
// first check snippets. That is a simple, non-recursive replacement
|
|
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
|
importedTokens = p.definedSnippets[importPattern]
|
|
if len(importedTokens) > 0 {
|
|
// just grab the first one
|
|
nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName))
|
|
}
|
|
} else {
|
|
// make path relative to the file of the _token_ being processed rather
|
|
// than current working directory (issue #867) and then use glob to get
|
|
// list of matching filenames
|
|
absFile, err := caddy.FastAbs(p.Dispenser.File())
|
|
if err != nil {
|
|
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
|
|
}
|
|
|
|
var matches []string
|
|
var globPattern string
|
|
if !filepath.IsAbs(importPattern) {
|
|
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
|
} else {
|
|
globPattern = importPattern
|
|
}
|
|
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
|
|
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
|
|
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
|
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
|
}
|
|
matches, err = filepath.Glob(globPattern)
|
|
if err != nil {
|
|
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
|
}
|
|
if len(matches) == 0 {
|
|
if strings.ContainsAny(globPattern, "*?[]") {
|
|
caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
|
|
} else {
|
|
return p.Errf("File to import not found: %s", importPattern)
|
|
}
|
|
} else {
|
|
// See issue #5295 - should skip any files that start with a . when iterating over them.
|
|
sep := string(filepath.Separator)
|
|
segGlobPattern := strings.Split(globPattern, sep)
|
|
if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") {
|
|
var tmpMatches []string
|
|
for _, m := range matches {
|
|
seg := strings.Split(m, sep)
|
|
if !strings.HasPrefix(seg[len(seg)-1], ".") {
|
|
tmpMatches = append(tmpMatches, m)
|
|
}
|
|
}
|
|
matches = tmpMatches
|
|
}
|
|
}
|
|
|
|
// collect all the imported tokens
|
|
for _, importFile := range matches {
|
|
newTokens, err := p.doSingleImport(importFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
importedTokens = append(importedTokens, newTokens...)
|
|
}
|
|
nodes = matches
|
|
}
|
|
|
|
nodeName := p.File()
|
|
if p.Token().snippetName != "" {
|
|
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
|
}
|
|
p.importGraph.addNode(nodeName)
|
|
p.importGraph.addNodes(nodes)
|
|
if err := p.importGraph.addEdges(nodeName, nodes); err != nil {
|
|
p.importGraph.removeNodes(nodes)
|
|
return err
|
|
}
|
|
|
|
// copy the tokens so we don't overwrite p.definedSnippets
|
|
tokensCopy := make([]Token, 0, len(importedTokens))
|
|
|
|
var (
|
|
maybeSnippet bool
|
|
maybeSnippetId bool
|
|
index int
|
|
)
|
|
|
|
// run the argument replacer on the tokens
|
|
// golang for range slice return a copy of value
|
|
// similarly, append also copy value
|
|
for i, token := range importedTokens {
|
|
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
|
|
if token.snippetName != "" {
|
|
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
|
|
} else {
|
|
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
|
|
}
|
|
|
|
// naive way of determine snippets, as snippets definition can only follow name + block
|
|
// format, won't check for nesting correctness or any other error, that's what parser does.
|
|
if !maybeSnippet && nesting == 0 {
|
|
// first of the line
|
|
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
|
index = 0
|
|
} else {
|
|
index++
|
|
}
|
|
|
|
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
|
|
maybeSnippetId = true
|
|
}
|
|
}
|
|
|
|
switch token.Text {
|
|
case "{":
|
|
nesting++
|
|
if index == 1 && maybeSnippetId && nesting == 1 {
|
|
maybeSnippet = true
|
|
maybeSnippetId = false
|
|
}
|
|
case "}":
|
|
nesting--
|
|
if nesting == 0 && maybeSnippet {
|
|
maybeSnippet = false
|
|
}
|
|
}
|
|
// if it is {block}, we substitute with all tokens in the block
|
|
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
|
var skip bool
|
|
var tokensToAdd []Token
|
|
switch {
|
|
case token.Text == "{block}":
|
|
tokensToAdd = blockTokens
|
|
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
|
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
|
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
|
val, ok := blockMapping[blockKey]
|
|
if ok {
|
|
tokensToAdd = val
|
|
}
|
|
default:
|
|
skip = true
|
|
}
|
|
if !skip {
|
|
if len(tokensToAdd) == 0 {
|
|
// if there is no content in the snippet block, don't do any replacement
|
|
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
|
|
tokensCopy = append(tokensCopy, token)
|
|
} else {
|
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if maybeSnippet {
|
|
tokensCopy = append(tokensCopy, token)
|
|
continue
|
|
}
|
|
|
|
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
|
|
if foundVariadic {
|
|
for _, arg := range args[startIndex:endIndex] {
|
|
token.Text = arg
|
|
tokensCopy = append(tokensCopy, token)
|
|
}
|
|
} else {
|
|
token.Text = repl.ReplaceKnown(token.Text, "")
|
|
tokensCopy = append(tokensCopy, token)
|
|
}
|
|
}
|
|
|
|
// splice the imported tokens in the place of the import statement
|
|
// and rewind cursor so Next() will land on first imported token
|
|
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
|
|
p.cursor -= len(args) + len(blockTokens) + 1
|
|
|
|
return nil
|
|
}
|
|
|
|
// doSingleImport lexes the individual file at importFile and returns
|
|
// its tokens or an error, if any.
|
|
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|
file, err := os.Open(importFile)
|
|
if err != nil {
|
|
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
if info, err := file.Stat(); err != nil {
|
|
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
|
} else if info.IsDir() {
|
|
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
|
}
|
|
|
|
input, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
|
}
|
|
|
|
// only warning in case of empty files
|
|
if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 {
|
|
caddy.Log().Warn("Import file is empty", zap.String("file", importFile))
|
|
return []Token{}, nil
|
|
}
|
|
|
|
importedTokens, err := allTokens(importFile, input)
|
|
if err != nil {
|
|
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
|
}
|
|
|
|
// Tack the file path onto these tokens so errors show the imported file's name
|
|
// (we use full, absolute path to avoid bugs: issue #1892)
|
|
filename, err := caddy.FastAbs(importFile)
|
|
if err != nil {
|
|
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
|
}
|
|
for i := 0; i < len(importedTokens); i++ {
|
|
importedTokens[i].File = filename
|
|
}
|
|
|
|
return importedTokens, nil
|
|
}
|
|
|
|
// directive collects tokens until the directive's scope
|
|
// closes (either end of line or end of curly brace block).
|
|
// It expects the currently-loaded token to be a directive
|
|
// (or } that ends a server block). The collected tokens
|
|
// are loaded into the current server block for later use
|
|
// by directive setup functions.
|
|
func (p *parser) directive() error {
|
|
// a segment is a list of tokens associated with this directive
|
|
var segment Segment
|
|
|
|
// the directive itself is appended as a relevant token
|
|
segment = append(segment, p.Token())
|
|
|
|
for p.Next() {
|
|
if p.Val() == "{" {
|
|
p.nesting++
|
|
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
|
return p.Err("Unexpected next token after '{' on same line")
|
|
}
|
|
if p.isNewLine() {
|
|
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
|
|
}
|
|
} else if p.Val() == "{}" {
|
|
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
|
return p.Err("Unexpected '{}' at end of line")
|
|
}
|
|
} else if p.isNewLine() && p.nesting == 0 {
|
|
p.cursor-- // read too far
|
|
break
|
|
} else if p.Val() == "}" && p.nesting > 0 {
|
|
p.nesting--
|
|
} else if p.Val() == "}" && p.nesting == 0 {
|
|
return p.Err("Unexpected '}' because no matching opening brace")
|
|
} else if p.Val() == "import" && p.isNewLine() {
|
|
if err := p.doImport(1); err != nil {
|
|
return err
|
|
}
|
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
|
continue
|
|
}
|
|
|
|
segment = append(segment, p.Token())
|
|
}
|
|
|
|
p.block.Segments = append(p.block.Segments, segment)
|
|
|
|
if p.nesting > 0 {
|
|
return p.EOFErr()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// openCurlyBrace expects the current token to be an
|
|
// opening curly brace. This acts like an assertion
|
|
// because it returns an error if the token is not
|
|
// a opening curly brace. It does NOT advance the token.
|
|
func (p *parser) openCurlyBrace() error {
|
|
if p.Val() != "{" {
|
|
return p.SyntaxErr("{")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// closeCurlyBrace expects the current token to be
|
|
// a closing curly brace. This acts like an assertion
|
|
// because it returns an error if the token is not
|
|
// a closing curly brace. It does NOT advance the token.
|
|
func (p *parser) closeCurlyBrace() error {
|
|
if p.Val() != "}" {
|
|
return p.SyntaxErr("}")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) isNamedRoute() (bool, string) {
|
|
keys := p.block.Keys
|
|
// A named route block is a single key with parens, prefixed with &.
|
|
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "&(") && strings.HasSuffix(keys[0].Text, ")") {
|
|
return true, strings.TrimSuffix(keys[0].Text[2:], ")")
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
func (p *parser) isSnippet() (bool, string) {
|
|
keys := p.block.Keys
|
|
// A snippet block is a single key with parens. Nothing else qualifies.
|
|
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "(") && strings.HasSuffix(keys[0].Text, ")") {
|
|
return true, strings.TrimSuffix(keys[0].Text[1:], ")")
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
// read and store everything in a block for later replay.
|
|
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
|
|
// block must have curlies.
|
|
err := p.openCurlyBrace()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nesting := 1 // count our own nesting
|
|
tokens := []Token{}
|
|
if retainCurlies {
|
|
tokens = append(tokens, p.Token())
|
|
}
|
|
for p.Next() {
|
|
if p.Val() == "}" {
|
|
nesting--
|
|
if nesting == 0 {
|
|
if retainCurlies {
|
|
tokens = append(tokens, p.Token())
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if p.Val() == "{" {
|
|
nesting++
|
|
}
|
|
tokens = append(tokens, p.tokens[p.cursor])
|
|
}
|
|
// make sure we're matched up
|
|
if nesting != 0 {
|
|
return nil, p.SyntaxErr("}")
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
// ServerBlock associates any number of keys from the
|
|
// head of the server block with tokens, which are
|
|
// grouped by segments.
|
|
type ServerBlock struct {
|
|
HasBraces bool
|
|
Keys []Token
|
|
Segments []Segment
|
|
IsNamedRoute bool
|
|
}
|
|
|
|
func (sb ServerBlock) GetKeysText() []string {
|
|
res := []string{}
|
|
for _, k := range sb.Keys {
|
|
res = append(res, k.Text)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// DispenseDirective returns a dispenser that contains
|
|
// all the tokens in the server block.
|
|
func (sb ServerBlock) DispenseDirective(dir string) *Dispenser {
|
|
var tokens []Token
|
|
for _, seg := range sb.Segments {
|
|
if len(seg) > 0 && seg[0].Text == dir {
|
|
tokens = append(tokens, seg...)
|
|
}
|
|
}
|
|
return NewDispenser(tokens)
|
|
}
|
|
|
|
// Segment is a list of tokens which begins with a directive
|
|
// and ends at the end of the directive (either at the end of
|
|
// the line, or at the end of a block it opens).
|
|
type Segment []Token
|
|
|
|
// Directive returns the directive name for the segment.
|
|
// The directive name is the text of the first token.
|
|
func (s Segment) Directive() string {
|
|
if len(s) > 0 {
|
|
return s[0].Text
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// spanOpen and spanClose are used to bound spans that
|
|
// contain the name of an environment variable.
|
|
var (
|
|
spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
|
|
envVarDefaultDelimiter = ":"
|
|
)
|