package namedconf import ( "errors" "fmt" "strings" ) // Parse parses a complete configuration (or configuration fragment). The // file name is only used in error messages and positions. Include // directives are returned as ordinary statements; callers that want to // follow them must do so themselves. func Parse(file, src string) ([]*Stmt, error) { p := &parser{lex: &lexer{src: src, line: 1, file: file}} if err := p.advance(); err != nil { return nil, err } stmts, err := p.stmts(false) if err != nil { return nil, err } return stmts, nil } type parser struct { lex *lexer tok token } func (p *parser) advance() error { t, err := p.lex.next() if err != nil { return err } p.tok = t return nil } func (p *parser) errorf(format string, args ...any) error { return &Error{Pos: p.tok.pos, Msg: fmt.Sprintf(format, args...)} } // stmts parses statements until EOF (top level) or a closing brace. func (p *parser) stmts(inBlock bool) ([]*Stmt, error) { var out []*Stmt for { switch p.tok.kind { case tokEOF: if inBlock { return nil, p.errorf("missing '}' (unexpected end of input)") } return out, nil case tokRBrace: if !inBlock { return nil, p.errorf("unexpected '}'") } return out, nil case tokSemi: // Empty statement; BIND tolerates stray semicolons. if err := p.advance(); err != nil { return nil, err } continue } st, err := p.stmt() if err != nil { return nil, err } out = append(out, st) } } func (p *parser) stmt() (*Stmt, error) { st := &Stmt{Pos: p.tok.pos} for { switch p.tok.kind { case tokSemi: if err := p.advance(); err != nil { return nil, err } return st, nil case tokEOF: return nil, &Error{Pos: st.Pos, Msg: "missing ';' at end of statement"} case tokRBrace: return nil, p.errorf("missing ';' before '}'") case tokWord: st.Items = append(st.Items, Item{Value: &Value{Text: p.tok.text, Pos: p.tok.pos}}) case tokString: st.Items = append(st.Items, Item{Value: &Value{Text: p.tok.text, Quoted: true, Pos: p.tok.pos}}) case tokBang: st.Items = append(st.Items, Item{Bang: true}) case tokLBrace: pos := p.tok.pos if err := p.advance(); err != nil { return nil, err } inner, err := p.stmts(true) if err != nil { return nil, err } st.Items = append(st.Items, Item{Block: &Block{Stmts: inner, Pos: pos}}) // p.tok is now '}'. } if err := p.advance(); err != nil { return nil, err } } } // ParseStatement parses exactly one statement. A missing trailing // semicolon is added. func ParseStatement(src string) (*Stmt, error) { s := strings.TrimSpace(src) if !strings.HasSuffix(s, ";") { s += ";" } stmts, err := Parse("", s) if err != nil { return nil, err } if len(stmts) != 1 { return nil, fmt.Errorf("expected exactly one statement, found %d", len(stmts)) } return stmts[0], nil } // ParseOptionValue parses the value part of an option (everything after // the option name), for example `{ 10.0.0.0/8; localhost; }` for // allow-query. It returns the statement "name value;". func ParseOptionValue(name, value string) (*Stmt, error) { v := strings.TrimSpace(value) v = strings.TrimSuffix(v, ";") if strings.TrimSpace(v) == "" { return nil, fmt.Errorf("option %s needs a value", name) } st, err := ParseStatement(name + " " + v) if err != nil { var pe *Error if errors.As(err, &pe) { return nil, errors.New(pe.Msg) } return nil, err } if st.Keyword() != strings.ToLower(name) { return nil, fmt.Errorf("value for %s is malformed", name) } return st, nil }