package scfg import ( "bufio" "fmt" "io" "os" "strings" ) // This limits the max block nesting depth to prevent stack overflows. const maxNestingDepth = 1000 // Load loads a configuration file. func Load(path string) (Block, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() return Read(f) } // Read parses a configuration file from an io.Reader. func Read(r io.Reader) (Block, error) { scanner := bufio.NewScanner(r) dec := decoder{scanner: scanner} block, closingBrace, err := dec.readBlock() if err != nil { return nil, err } else if closingBrace { return nil, fmt.Errorf("line %v: unexpected '}'", dec.lineno) } return block, scanner.Err() } type decoder struct { scanner *bufio.Scanner lineno int blockDepth int } // readBlock reads a block. closingBrace is true if parsing stopped on '}' // (otherwise, it stopped on Scanner.Scan). func (dec *decoder) readBlock() (block Block, closingBrace bool, err error) { dec.blockDepth++ defer func() { dec.blockDepth-- }() if dec.blockDepth >= maxNestingDepth { return nil, false, fmt.Errorf("exceeded max block depth") } for dec.scanner.Scan() { dec.lineno++ l := dec.scanner.Text() words, err := splitWords(l) if err != nil { return nil, false, fmt.Errorf("line %v: %v", dec.lineno, err) } else if len(words) == 0 { continue } if len(words) == 1 && l[len(l)-1] == '}' { closingBrace = true break } var d *Directive if words[len(words)-1] == "{" && l[len(l)-1] == '{' { words = words[:len(words)-1] var name string params := words if len(words) > 0 { name, params = words[0], words[1:] } startLineno := dec.lineno childBlock, childClosingBrace, err := dec.readBlock() if err != nil { return nil, false, err } else if !childClosingBrace { return nil, false, fmt.Errorf("line %v: unterminated block", startLineno) } // Allows callers to tell apart "no block" and "empty block" if childBlock == nil { childBlock = Block{} } d = &Directive{Name: name, Params: params, Children: childBlock, lineno: dec.lineno} } else { d = &Directive{Name: words[0], Params: words[1:], lineno: dec.lineno} } block = append(block, d) } return block, closingBrace, nil } func splitWords(l string) ([]string, error) { var ( words []string sb strings.Builder escape bool quote rune wantWSP bool ) for _, ch := range l { switch { case escape: sb.WriteRune(ch) escape = false case wantWSP && (ch != ' ' && ch != '\t'): return words, fmt.Errorf("atom not allowed after quoted string") case ch == '\\': escape = true case quote != 0 && ch == quote: quote = 0 wantWSP = true if sb.Len() == 0 { words = append(words, "") } case quote == 0 && len(words) == 0 && sb.Len() == 0 && ch == '#': return nil, nil case quote == 0 && (ch == '\'' || ch == '"'): if sb.Len() > 0 { return words, fmt.Errorf("quoted string not allowed after atom") } quote = ch case quote == 0 && (ch == ' ' || ch == '\t'): if sb.Len() > 0 { words = append(words, sb.String()) } sb.Reset() wantWSP = false default: sb.WriteRune(ch) } } if quote != 0 { return words, fmt.Errorf("unterminated quoted string") } if sb.Len() > 0 { words = append(words, sb.String()) } return words, nil }