package interpres import ( "fmt" "strconv" "strings" ) // parser is a recursive-descent TOML parser producing a map[string]any tree. type parser struct { src []rune pos int line int root map[string]any current map[string]any headers map[string]bool frozen map[string]bool dotted map[string]bool arrays map[string]bool currentPath []string } func (p *parser) parse() (map[string]any, error) { p.root = map[string]any{} p.current = p.root p.headers = map[string]bool{} p.frozen = map[string]bool{} p.dotted = map[string]bool{} p.arrays = map[string]bool{} p.currentPath = nil for { if err := p.skipBlank(); err != nil { return nil, err } if p.eof() { break } c := p.peek() switch { case c == '[': if err := p.parseTableHeader(); err != nil { return nil, err } default: if err := p.parseKeyValue(); err != nil { return nil, err } } if err := p.expectLineEnd(); err != nil { return nil, err } } return p.root, nil } // --- table headers --------------------------------------------------------- func (p *parser) parseTableHeader() error { array := false p.next() // consume '[' if !p.eof() && p.peek() == '[' { array = true p.next() } key, err := p.parseKeyPath() if err != nil { return err } p.skipInline() if p.eof() || p.peek() != ']' { return p.errf("expected ']' to close table header") } p.next() if array { if p.eof() || p.peek() != ']' { return p.errf("expected ']]' to close array-of-tables header") } p.next() } if array { tbl, err := p.appendArrayTable(key) if err != nil { return err } // A new array-of-tables element starts a fresh scope: sub-table headers // and inline-table freezes from the previous element no longer apply. p.resetScopeUnder(key) p.arrays[pathKey(key)] = true p.current = tbl p.currentPath = key return nil } pk := pathKey(key) if p.headers[pk] || p.dotted[pk] || p.arrays[pk] { return p.errf("table %q is defined more than once", strings.Join(key, ".")) } p.headers[pk] = true tbl, err := p.tableAt(key) if err != nil { return err } p.current = tbl p.currentPath = key return nil } // tableAt walks (creating intermediate tables) to the table named by key, // relative to the document root, rejecting any step into a frozen inline table. func (p *parser) tableAt(key []string) (map[string]any, error) { cur := p.root path := make([]string, 0, len(key)) for _, k := range key { path = append(path, k) if p.frozen[pathKey(path)] { return nil, p.errf("cannot extend inline table %q", strings.Join(path, ".")) } existing, ok := cur[k] if !ok { next := map[string]any{} cur[k] = next cur = next continue } switch v := existing.(type) { case map[string]any: cur = v case []map[string]any: if len(v) == 0 { return nil, p.errf("key %q is an empty array of tables", k) } cur = v[len(v)-1] default: return nil, p.errf("key %q is not a table", k) } } return cur, nil } func (p *parser) appendArrayTable(key []string) (map[string]any, error) { parent := p.root for _, k := range key[:len(key)-1] { existing, ok := parent[k] if !ok { next := map[string]any{} parent[k] = next parent = next continue } switch v := existing.(type) { case map[string]any: parent = v case []map[string]any: parent = v[len(v)-1] default: return nil, p.errf("key %q is not a table", k) } } leaf := key[len(key)-1] tbl := map[string]any{} switch existing := parent[leaf].(type) { case nil: parent[leaf] = []map[string]any{tbl} case []map[string]any: parent[leaf] = append(existing, tbl) default: return nil, p.errf("key %q is not an array of tables", leaf) } return tbl, nil } // --- key/value ------------------------------------------------------------- func (p *parser) parseKeyValue() error { key, err := p.parseKeyPath() if err != nil { return err } p.skipInline() if p.eof() || p.peek() != '=' { return p.errf("expected '=' after key") } p.next() p.skipInline() val, err := p.parseValue() if err != nil { return err } dest := p.current abs := append([]string{}, p.currentPath...) for _, k := range key[:len(key)-1] { abs = append(abs, k) if p.frozen[pathKey(abs)] { return p.errf("cannot extend inline table %q", strings.Join(abs, ".")) } if p.headers[pathKey(abs)] { return p.errf("cannot extend table %q with a dotted key", strings.Join(abs, ".")) } p.dotted[pathKey(abs)] = true existing, ok := dest[k] if !ok { next := map[string]any{} dest[k] = next dest = next continue } m, ok := existing.(map[string]any) if !ok { return p.errf("key %q is not a table", k) } dest = m } leaf := key[len(key)-1] abs = append(abs, leaf) if _, exists := dest[leaf]; exists { return p.errf("duplicate key %q", leaf) } dest[leaf] = val p.freezeInline(abs, val) return nil } // freezeInline marks the path of an inline table (and any nested inline tables) // as immutable, so a later header or dotted key cannot extend it. func (p *parser) freezeInline(path []string, val any) { m, ok := val.(map[string]any) if !ok { return } p.frozen[pathKey(path)] = true for k, v := range m { child := append(append([]string{}, path...), k) p.freezeInline(child, v) } } // resetScopeUnder forgets the header and freeze records nested under key, which // belong to the previous element of an array of tables. func (p *parser) resetScopeUnder(key []string) { prefix := pathKey(key) + "\x00" for k := range p.headers { if strings.HasPrefix(k, prefix) { delete(p.headers, k) } } for k := range p.frozen { if strings.HasPrefix(k, prefix) { delete(p.frozen, k) } } } // parseKeyPath parses a dotted key into its components. func (p *parser) parseKeyPath() ([]string, error) { var parts []string for { p.skipInline() part, err := p.parseKeyComponent() if err != nil { return nil, err } parts = append(parts, part) p.skipInline() if !p.eof() && p.peek() == '.' { p.next() continue } break } return parts, nil } func (p *parser) parseKeyComponent() (string, error) { if p.eof() { return "", p.errf("expected a key") } switch c := p.peek(); c { case '"': if p.lookahead(`"""`) { return "", p.errf("multiline strings are not allowed in keys") } return p.parseBasicString() case '\'': if p.lookahead(`'''`) { return "", p.errf("multiline strings are not allowed in keys") } return p.parseLiteralString() default: start := p.pos for !p.eof() { c := p.peek() if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' { p.next() continue } break } if p.pos == start { return "", p.errf("invalid key character %q", string(p.peek())) } return string(p.src[start:p.pos]), nil } } // --- values ---------------------------------------------------------------- func (p *parser) parseValue() (any, error) { if p.eof() { return nil, p.errf("expected a value") } switch c := p.peek(); { case c == '"': return p.parseBasicString() case c == '\'': return p.parseLiteralString() case c == '[': return p.parseArray() case c == '{': return p.parseInlineTable() case c == 't' || c == 'f': return p.parseBool() default: return p.parseAtom() } } func (p *parser) parseBool() (any, error) { if p.match("true") { return true, nil } if p.match("false") { return false, nil } return nil, p.errf("invalid value") } // parseAtom handles numbers, inf/nan, and date-times. func (p *parser) parseAtom() (any, error) { start := p.pos p.scanBareToken() tok := string(p.src[start:p.pos]) if tok == "" { return nil, p.errf("expected a value") } // A date may be followed by a space and a time, forming one date-time. if isDateToken(tok) && !p.eof() && p.peek() == ' ' && p.pos+1 < len(p.src) && p.src[p.pos+1] >= '0' && p.src[p.pos+1] <= '9' { p.next() // consume the separating space timeStart := p.pos p.scanBareToken() tok = tok + " " + string(p.src[timeStart:p.pos]) } if v, ok := parseDateTime(tok); ok { return v, nil } v, err := decodeNumber(tok) if err != nil { return nil, p.errf("%s", err) } return v, nil } // scanBareToken advances past a bare value token (number, bool, or date-time), // stopping at whitespace, a separator, or a comment. func (p *parser) scanBareToken() { for !p.eof() { c := p.peek() if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ']' || c == '}' || c == '#' { return } p.next() } } // --- strings --------------------------------------------------------------- func (p *parser) parseBasicString() (string, error) { if p.lookahead(`"""`) { return p.parseMultilineString('"', true) } p.next() // opening quote var b strings.Builder for { if p.eof() { return "", p.errf("unterminated string") } c := p.next() switch c { case '"': return b.String(), nil case '\n': return "", p.errf("unterminated string") case '\r': return "", p.errf("bare carriage return is not allowed in a string") case '\\': r, err := p.readEscape() if err != nil { return "", err } b.WriteRune(r) default: if isControlRune(c) { return "", p.errf("control character U+%04X is not allowed in a string", c) } b.WriteRune(c) } } } func (p *parser) parseLiteralString() (string, error) { if p.lookahead(`'''`) { return p.parseMultilineString('\'', false) } p.next() // opening quote var b strings.Builder for { if p.eof() { return "", p.errf("unterminated literal string") } c := p.next() if c == '\'' { return b.String(), nil } if c == '\n' { return "", p.errf("unterminated literal string") } if c == '\r' { return "", p.errf("bare carriage return is not allowed in a string") } if isControlRune(c) { return "", p.errf("control character U+%04X is not allowed in a string", c) } b.WriteRune(c) } } func (p *parser) parseMultilineString(quote rune, escapes bool) (string, error) { p.skipN(3) // opening delimiter // A newline immediately after the opening delimiter is trimmed. if !p.eof() && p.peek() == '\r' { p.next() } if !p.eof() && p.peek() == '\n' { p.line++ p.next() } var b strings.Builder for { if p.eof() { return "", p.errf("unterminated multiline string") } if p.peek() == quote { // Count the run of delimiter characters. The last three close the // string; up to two extra ones belong to the content. n := 0 for p.pos+n < len(p.src) && p.src[p.pos+n] == quote { n++ } if n >= 3 { if n > 5 { return "", p.errf("too many '%c' before the closing delimiter", quote) } for i := 0; i < n-3; i++ { b.WriteRune(quote) } p.skipN(n) return b.String(), nil } for i := 0; i < n; i++ { b.WriteRune(quote) p.next() } continue } c := p.next() if c == '\n' { p.line++ b.WriteRune(c) continue } if c == '\r' { if !p.eof() && p.peek() == '\n' { b.WriteRune(c) continue } return "", p.errf("bare carriage return is not allowed in a string") } if escapes && c == '\\' { // Line-ending backslash trims the following whitespace/newlines. if p.trimLineEndingBackslash() { continue } r, err := p.readEscape() if err != nil { return "", err } b.WriteRune(r) continue } if isControlRune(c) { return "", p.errf("control character U+%04X is not allowed in a string", c) } b.WriteRune(c) } } // trimLineEndingBackslash consumes whitespace through the next newline (and the // blank lines that follow) when a backslash is the last token on a line. // It reports whether it did so. func (p *parser) trimLineEndingBackslash() bool { save, saveLine := p.pos, p.line for !p.eof() { c := p.peek() if c == ' ' || c == '\t' || c == '\r' { p.next() continue } if c == '\n' { break } // Not a line-ending backslash; restore. p.pos, p.line = save, saveLine return false } if p.eof() { p.pos, p.line = save, saveLine return false } // Consume the newline and all following whitespace. for !p.eof() { c := p.peek() if c == '\n' { p.line++ p.next() continue } if c == ' ' || c == '\t' || c == '\r' { p.next() continue } break } return true } func (p *parser) readEscape() (rune, error) { if p.eof() { return 0, p.errf("unterminated escape sequence") } c := p.next() switch c { case 'b': return '\b', nil case 't': return '\t', nil case 'n': return '\n', nil case 'f': return '\f', nil case 'r': return '\r', nil case '"': return '"', nil case '\\': return '\\', nil case 'u': return p.readUnicode(4) case 'U': return p.readUnicode(8) default: return 0, p.errf("invalid escape sequence \\%c", c) } } func (p *parser) readUnicode(n int) (rune, error) { if p.pos+n > len(p.src) { return 0, p.errf("invalid unicode escape") } hex := string(p.src[p.pos : p.pos+n]) p.pos += n v, err := strconv.ParseInt(hex, 16, 64) if err != nil { return 0, p.errf("invalid unicode escape \\%s", hex) } if v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF) { return 0, p.errf("escape \\%s is not a valid Unicode scalar value", hex) } return rune(v), nil } // --- arrays and inline tables --------------------------------------------- func (p *parser) parseArray() (any, error) { p.next() // '[' arr := []any{} for { if err := p.skipArraySpace(); err != nil { return nil, err } if p.eof() { return nil, p.errf("unterminated array") } if p.peek() == ']' { p.next() return arr, nil } v, err := p.parseValue() if err != nil { return nil, err } arr = append(arr, v) if err := p.skipArraySpace(); err != nil { return nil, err } if p.eof() { return nil, p.errf("unterminated array") } switch p.peek() { case ',': p.next() case ']': p.next() return arr, nil default: return nil, p.errf("expected ',' or ']' in array") } } } func (p *parser) parseInlineTable() (any, error) { p.next() // '{' tbl := map[string]any{} assigned := map[string]bool{} p.skipInline() if !p.eof() && p.peek() == '}' { p.next() return tbl, nil } for { p.skipInline() key, err := p.parseKeyPath() if err != nil { return nil, err } p.skipInline() if p.eof() || p.peek() != '=' { return nil, p.errf("expected '=' in inline table") } p.next() p.skipInline() val, err := p.parseValue() if err != nil { return nil, err } dest := tbl path := make([]string, 0, len(key)) for _, k := range key[:len(key)-1] { path = append(path, k) if assigned[pathKey(path)] { return nil, p.errf("key %q is already defined", strings.Join(path, ".")) } existing, ok := dest[k] if !ok { m := map[string]any{} dest[k] = m dest = m continue } m, isMap := existing.(map[string]any) if !isMap { return nil, p.errf("key %q is already defined", k) } dest = m } leaf := key[len(key)-1] path = append(path, leaf) if _, exists := dest[leaf]; exists { return nil, p.errf("duplicate key %q in inline table", leaf) } dest[leaf] = val assigned[pathKey(path)] = true p.skipInline() if p.eof() { return nil, p.errf("unterminated inline table") } switch p.peek() { case ',': p.next() case '}': p.next() return tbl, nil default: return nil, p.errf("expected ',' or '}' in inline table") } } } // --- scanning helpers ------------------------------------------------------ func (p *parser) eof() bool { return p.pos >= len(p.src) } func (p *parser) peek() rune { return p.src[p.pos] } func (p *parser) next() rune { c := p.src[p.pos] p.pos++ return c } func (p *parser) skipN(n int) { for i := 0; i < n && !p.eof(); i++ { p.next() } } func (p *parser) match(word string) bool { if p.lookahead(word) { p.skipN(len([]rune(word))) return true } return false } func (p *parser) lookahead(s string) bool { r := []rune(s) if p.pos+len(r) > len(p.src) { return false } for i, c := range r { if p.src[p.pos+i] != c { return false } } return true } // skipInline consumes spaces and tabs only. func (p *parser) skipInline() { for !p.eof() { if c := p.peek(); c == ' ' || c == '\t' { p.next() continue } break } } // skipArraySpace consumes whitespace, newlines, and comments inside arrays. func (p *parser) skipArraySpace() error { for !p.eof() { switch p.peek() { case ' ', '\t': p.next() case '\r': if err := p.expectCRLF(); err != nil { return err } case '\n': p.line++ p.next() case '#': if err := p.skipComment(); err != nil { return err } default: return nil } } return nil } // skipBlank consumes whitespace, blank lines, and comments between statements. func (p *parser) skipBlank() error { for !p.eof() { switch p.peek() { case ' ', '\t': p.next() case '\r': if err := p.expectCRLF(); err != nil { return err } case '\n': p.line++ p.next() case '#': if err := p.skipComment(); err != nil { return err } default: return nil } } return nil } func (p *parser) skipComment() error { p.next() // consume '#' for !p.eof() { c := p.peek() switch { case c == '\n': return nil case c == '\r': if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' { return nil } return p.errf("bare carriage return is not allowed") case c == '\t': p.next() case c < 0x20 || c == 0x7f: return p.errf("control character U+%04X is not allowed in a comment", c) default: p.next() } } return nil } // expectCRLF consumes a carriage return that must be immediately followed by a // line feed; a bare CR is invalid. func (p *parser) expectCRLF() error { if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' { p.next() // consume CR; the LF is handled by the caller return nil } return p.errf("bare carriage return is not allowed") } // expectLineEnd consumes trailing inline whitespace and an optional comment, // then requires a newline or end of input. func (p *parser) expectLineEnd() error { p.skipInline() if p.eof() { return nil } if p.peek() == '#' { if err := p.skipComment(); err != nil { return err } } if p.eof() { return nil } if p.peek() == '\r' { if err := p.expectCRLF(); err != nil { return err } } if p.eof() { return nil } if p.peek() == '\n' { p.line++ p.next() return nil } return p.errf("unexpected %q after value", string(p.peek())) } func (p *parser) errf(format string, args ...any) error { return &SyntaxError{Line: p.line, Msg: fmt.Sprintf(format, args...)} } // pathKey joins key components with a NUL separator so a dotted path can be // used as a map key for tracking defined tables. func pathKey(parts []string) string { return strings.Join(parts, "\x00") } // isControlRune reports whether r is a control character disallowed in a string // literal. Tab, line feed, and carriage return are permitted (handled // elsewhere); everything else below U+0020, plus U+007F, is rejected. func isControlRune(r rune) bool { if r == '\t' || r == '\n' || r == '\r' { return false } return r < 0x20 || r == 0x7f }