// Package namedconf parses and prints the named.conf configuration syntax. // // The parser is deliberately generic: a configuration is a list of // statements, and each statement is a sequence of values (words or quoted // strings) and blocks, terminated by a semicolon. This is enough to // represent every BIND 9 statement without knowing its grammar, which is // checked separately by the options catalog and, ultimately, by // named-checkconf. package namedconf import ( "fmt" "strings" ) // Pos is a position in a configuration file. type Pos struct { File string Line int } func (p Pos) String() string { if p.File == "" { return fmt.Sprintf("line %d", p.Line) } return fmt.Sprintf("%s:%d", p.File, p.Line) } type tokenKind int const ( tokEOF tokenKind = iota tokWord tokString tokLBrace tokRBrace tokSemi tokBang ) func (k tokenKind) String() string { switch k { case tokEOF: return "end of input" case tokWord: return "word" case tokString: return "quoted string" case tokLBrace: return "'{'" case tokRBrace: return "'}'" case tokSemi: return "';'" case tokBang: return "'!'" } return "?" } type token struct { kind tokenKind text string pos Pos } // Error is a syntax error with a position. type Error struct { Pos Pos Msg string } func (e *Error) Error() string { return e.Pos.String() + ": " + e.Msg } type lexer struct { src string i int line int file string } func (l *lexer) errorf(format string, args ...any) error { return &Error{Pos: Pos{File: l.file, Line: l.line}, Msg: fmt.Sprintf(format, args...)} } // next returns the next token, skipping whitespace and all three comment // styles (/* */, // and #). func (l *lexer) next() (token, error) { for { if l.i >= len(l.src) { return token{kind: tokEOF, pos: Pos{l.file, l.line}}, nil } c := l.src[l.i] switch { case c == '\n': l.line++ l.i++ case c == ' ' || c == '\t' || c == '\r' || c == '\f' || c == '\v': l.i++ case c == '#': l.skipLine() case c == '/' && l.peek(1) == '/': l.skipLine() case c == '/' && l.peek(1) == '*': start := l.line end := strings.Index(l.src[l.i+2:], "*/") if end < 0 { l.line = start return token{}, l.errorf("unterminated /* comment") } l.line += strings.Count(l.src[l.i:l.i+2+end], "\n") l.i += 2 + end + 2 default: return l.lexToken() } } } func (l *lexer) peek(n int) byte { if l.i+n < len(l.src) { return l.src[l.i+n] } return 0 } func (l *lexer) skipLine() { for l.i < len(l.src) && l.src[l.i] != '\n' { l.i++ } } func (l *lexer) lexToken() (token, error) { pos := Pos{l.file, l.line} c := l.src[l.i] switch c { case '{': l.i++ return token{tokLBrace, "{", pos}, nil case '}': l.i++ return token{tokRBrace, "}", pos}, nil case ';': l.i++ return token{tokSemi, ";", pos}, nil case '!': l.i++ return token{tokBang, "!", pos}, nil case '"': var sb strings.Builder l.i++ for { if l.i >= len(l.src) { return token{}, &Error{Pos: pos, Msg: "unterminated quoted string"} } c := l.src[l.i] if c == '\\' && l.i+1 < len(l.src) { sb.WriteByte(l.src[l.i+1]) if l.src[l.i+1] == '\n' { l.line++ } l.i += 2 continue } if c == '"' { l.i++ break } if c == '\n' { l.line++ } sb.WriteByte(c) l.i++ } return token{tokString, sb.String(), pos}, nil } start := l.i for l.i < len(l.src) { c := l.src[l.i] if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v' || c == '{' || c == '}' || c == ';' || c == '"' || c == '!' || c == '#' { break } if c == '/' && (l.peek(1) == '/' || l.peek(1) == '*') { break } l.i++ } return token{tokWord, l.src[start:l.i], pos}, nil }