436 lines
11 KiB
Go
436 lines
11 KiB
Go
package toml
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
)
|
|
|
|
// Enforce 64-bit platform; fails compilation on 32-bit targets where int(int64) in parseInteger would truncate
|
|
const _ uint = 1<<63 - 1
|
|
|
|
// Parser parses TOML tokens into a map[string]any
|
|
type Parser struct {
|
|
root map[string]any
|
|
lexer *Lexer
|
|
current any // Pointer to the current map or slice of maps being populated (scope)
|
|
// Identity set of inline-table maps (immutable per TOML spec)
|
|
frozen map[uintptr]bool
|
|
curToken Token
|
|
peekToken Token
|
|
// Value nesting depth (arrays / inline tables)
|
|
depth int
|
|
}
|
|
|
|
// Recursion bound for parseValue -> parseArray/parseInlineTable
|
|
const maxValueDepth = 1000
|
|
|
|
func NewParser(input []byte) *Parser {
|
|
l := NewLexer(input)
|
|
p := &Parser{
|
|
lexer: l,
|
|
root: make(map[string]any),
|
|
frozen: make(map[uintptr]bool),
|
|
}
|
|
p.nextToken()
|
|
p.nextToken()
|
|
p.current = p.root
|
|
return p
|
|
}
|
|
|
|
func (p *Parser) nextToken() {
|
|
p.curToken = p.peekToken
|
|
p.peekToken = p.lexer.NextToken()
|
|
|
|
// Skip comments automatically
|
|
for p.peekToken.Type == TokenComment {
|
|
p.peekToken = p.lexer.NextToken()
|
|
}
|
|
}
|
|
|
|
func (p *Parser) Parse() (map[string]any, error) {
|
|
for p.curToken.Type != TokenEOF {
|
|
if p.curToken.Type == TokenNewline {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
|
|
if err := p.parseStatement(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return p.root, nil
|
|
}
|
|
|
|
func (p *Parser) parseStatement() error {
|
|
switch p.curToken.Type {
|
|
case TokenLBracket:
|
|
// Table Definition: [table] or [[array.table]]
|
|
return p.parseTableDeclaration()
|
|
case TokenIdent, TokenString:
|
|
// Key-Value Pair: key = value
|
|
return p.parseKeyValuePair(p.current)
|
|
case TokenError:
|
|
return fmt.Errorf("lexing error line %d: %s", p.curToken.Line, p.curToken.Literal)
|
|
default:
|
|
return fmt.Errorf("unexpected token line %d: %s", p.curToken.Line, p.curToken.String())
|
|
}
|
|
}
|
|
|
|
// parseTableDeclaration handles [key] and [[key]]
|
|
func (p *Parser) parseTableDeclaration() error {
|
|
isArray := false
|
|
if p.peekToken.Type == TokenLBracket {
|
|
// It is [[ ...
|
|
p.nextToken() // consume first [
|
|
isArray = true
|
|
}
|
|
p.nextToken() // consume [
|
|
|
|
// Parse Key (dotted)
|
|
keys, err := p.parseKeyParts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isArray {
|
|
if p.curToken.Type != TokenRBracket {
|
|
return fmt.Errorf("expected closing bracket for array table at line %d", p.curToken.Line)
|
|
}
|
|
p.nextToken() // consume first ]
|
|
}
|
|
|
|
if p.curToken.Type != TokenRBracket {
|
|
return fmt.Errorf("expected closing bracket for table at line %d", p.curToken.Line)
|
|
}
|
|
p.nextToken() // consume final ]
|
|
|
|
// Define scope
|
|
return p.setTableScope(keys, isArray)
|
|
}
|
|
|
|
// setTableScope navigates/creates the map structure and sets p.current
|
|
func (p *Parser) setTableScope(keys []string, isArrayOfTables bool) error {
|
|
// Table declarations always start from root
|
|
var ptr any = p.root
|
|
|
|
for i, key := range keys {
|
|
isLast := i == len(keys)-1
|
|
currentMap, ok := ptr.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("key path conflict: %s is not a map", key)
|
|
}
|
|
|
|
if isLast {
|
|
if isArrayOfTables {
|
|
// [[a.b]] -> Ensure 'b' is a slice of maps, append new map, set cursor to it
|
|
var slice []map[string]any
|
|
if val, exists := currentMap[key]; exists {
|
|
if s, ok := val.([]map[string]any); ok {
|
|
slice = s
|
|
} else {
|
|
return fmt.Errorf("key conflict: %s is not an array of tables", key)
|
|
}
|
|
} else {
|
|
slice = make([]map[string]any, 0)
|
|
}
|
|
|
|
newMap := make(map[string]any)
|
|
slice = append(slice, newMap)
|
|
currentMap[key] = slice
|
|
p.current = newMap
|
|
} else {
|
|
// [a.b] -> Ensure 'b' is a map, set cursor to it
|
|
var targetMap map[string]any
|
|
if val, exists := currentMap[key]; exists {
|
|
if m, ok := val.(map[string]any); ok {
|
|
// Inline tables cannot be reopened
|
|
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
|
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
|
}
|
|
targetMap = m
|
|
} else {
|
|
return fmt.Errorf("key conflict: %s is not a table", key)
|
|
}
|
|
} else {
|
|
targetMap = make(map[string]any)
|
|
currentMap[key] = targetMap
|
|
}
|
|
p.current = targetMap
|
|
}
|
|
} else {
|
|
// Intermediate key -> ensure map exists and traverse.
|
|
// Traversal through an existing [[array]] descends into its last element.
|
|
if val, exists := currentMap[key]; exists {
|
|
if m, ok := val.(map[string]any); ok {
|
|
// Inline tables cannot be extended via sub-tables
|
|
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
|
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
|
}
|
|
ptr = m
|
|
} else if slice, ok := val.([]map[string]any); ok {
|
|
if len(slice) == 0 {
|
|
return fmt.Errorf("cannot traverse empty array table %s", key)
|
|
}
|
|
ptr = slice[len(slice)-1]
|
|
} else {
|
|
return fmt.Errorf("intermediate key %s is not a map", key)
|
|
}
|
|
} else {
|
|
newMap := make(map[string]any)
|
|
currentMap[key] = newMap
|
|
ptr = newMap
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseKeyValuePair(scope any) error {
|
|
// Parse Key (dotted allowed: a.b.c = 1)
|
|
keys, err := p.parseKeyParts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.curToken.Type != TokenEqual {
|
|
return fmt.Errorf("expected '=' after key at line %d, got %s", p.curToken.Line, p.curToken.String())
|
|
}
|
|
p.nextToken() // consume =
|
|
|
|
val, err := p.parseValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Assign value to scope
|
|
return p.assignValue(scope, keys, val)
|
|
}
|
|
|
|
func (p *Parser) assignValue(scope any, keys []string, val any) error {
|
|
ptr := scope
|
|
|
|
// If scope is map, easy. If scope is not map, error.
|
|
currentMap, ok := ptr.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("scope is not a map")
|
|
}
|
|
|
|
for i, key := range keys {
|
|
if i == len(keys)-1 {
|
|
// Final key, assign value
|
|
if _, exists := currentMap[key]; exists {
|
|
return fmt.Errorf("duplicate key %s at line %d", key, p.curToken.Line)
|
|
}
|
|
currentMap[key] = val
|
|
} else {
|
|
// Intermediate, ensure map
|
|
if existing, exists := currentMap[key]; exists {
|
|
if m, ok := existing.(map[string]any); ok {
|
|
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
|
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
|
}
|
|
currentMap = m
|
|
} else {
|
|
return fmt.Errorf("intermediate key %s is not a map", key)
|
|
}
|
|
} else {
|
|
newMap := make(map[string]any)
|
|
currentMap[key] = newMap
|
|
currentMap = newMap
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseKeyParts() ([]string, error) {
|
|
var keys []string
|
|
for {
|
|
// Rule: Tokens identified as Numbers are forbidden as keys
|
|
if p.curToken.Type == TokenInteger || p.curToken.Type == TokenFloat {
|
|
return nil, fmt.Errorf("numeric keys are forbidden: %q", p.curToken.Literal)
|
|
}
|
|
|
|
if p.curToken.Type == TokenString {
|
|
// Rule: Even quoted strings shouldn't be pure numbers per instruction
|
|
if _, err := strconv.Atoi(p.curToken.Literal); err == nil {
|
|
return nil, fmt.Errorf("numeric string keys are forbidden: %q", p.curToken.Literal)
|
|
}
|
|
}
|
|
|
|
if p.curToken.Type != TokenIdent && p.curToken.Type != TokenString {
|
|
return nil, fmt.Errorf("expected key, got %s", p.curToken.String())
|
|
}
|
|
|
|
keys = append(keys, p.curToken.Literal)
|
|
p.nextToken()
|
|
|
|
if p.curToken.Type == TokenDot {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
func (p *Parser) parseValue() (any, error) {
|
|
// Guard unbounded recursion on nested
|
|
p.depth++
|
|
defer func() { p.depth-- }()
|
|
if p.depth > maxValueDepth {
|
|
return nil, fmt.Errorf("value nesting exceeds %d at line %d", maxValueDepth, p.curToken.Line)
|
|
}
|
|
|
|
switch p.curToken.Type {
|
|
case TokenString:
|
|
val := p.curToken.Literal
|
|
p.nextToken()
|
|
return val, nil
|
|
case TokenInteger:
|
|
val, err := p.parseInteger(p.curToken.Literal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid integer %q at line %d: %w", p.curToken.Literal, p.curToken.Line, err)
|
|
}
|
|
p.nextToken()
|
|
return val, nil
|
|
case TokenFloat:
|
|
val, err := strconv.ParseFloat(p.curToken.Literal, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid float %q at line %d: %w", p.curToken.Literal, p.curToken.Line, err)
|
|
}
|
|
p.nextToken()
|
|
return val, nil
|
|
case TokenBool:
|
|
val := p.curToken.Literal == "true"
|
|
p.nextToken()
|
|
return val, nil
|
|
case TokenLBracket:
|
|
return p.parseArray()
|
|
case TokenLBrace:
|
|
return p.parseInlineTable()
|
|
}
|
|
return nil, fmt.Errorf("unexpected value token %s at line %d", p.curToken.String(), p.curToken.Line)
|
|
}
|
|
|
|
func (p *Parser) parseInteger(lit string) (int, error) {
|
|
// Handle optional leading sign
|
|
negative := false
|
|
numLit := lit
|
|
if len(numLit) > 0 && (numLit[0] == '+' || numLit[0] == '-') {
|
|
negative = numLit[0] == '-'
|
|
numLit = numLit[1:]
|
|
}
|
|
|
|
var val int64
|
|
var err error
|
|
|
|
if len(numLit) > 2 && numLit[0] == '0' {
|
|
switch numLit[1] {
|
|
case 'x', 'X':
|
|
val, err = strconv.ParseInt(numLit[2:], 16, 64)
|
|
case 'o', 'O':
|
|
val, err = strconv.ParseInt(numLit[2:], 8, 64)
|
|
case 'b', 'B':
|
|
val, err = strconv.ParseInt(numLit[2:], 2, 64)
|
|
default:
|
|
val, err = strconv.ParseInt(lit, 10, 64)
|
|
return int(val), err
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if negative {
|
|
val = -val
|
|
}
|
|
return int(val), nil
|
|
}
|
|
|
|
val, err = strconv.ParseInt(lit, 10, 64)
|
|
return int(val), err
|
|
}
|
|
|
|
func (p *Parser) parseArray() ([]any, error) {
|
|
p.nextToken() // consume [
|
|
arr := make([]any, 0)
|
|
|
|
for p.curToken.Type != TokenRBracket {
|
|
if p.curToken.Type == TokenNewline {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
|
|
val, err := p.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arr = append(arr, val)
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
p.nextToken()
|
|
} else if p.curToken.Type != TokenRBracket {
|
|
// Check for newlines between elements if missing comma? TOML usually requires comma.
|
|
// Relaxed parser: require comma unless followed immediately by bracket (trailing comma allowed)
|
|
if p.curToken.Type == TokenNewline {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("expected comma or closing bracket in array at line %d", p.curToken.Line)
|
|
}
|
|
}
|
|
p.nextToken() // consume ]
|
|
return arr, nil
|
|
}
|
|
|
|
func (p *Parser) parseInlineTable() (map[string]any, error) {
|
|
p.nextToken() // consume {
|
|
m := make(map[string]any)
|
|
|
|
for p.curToken.Type != TokenRBrace {
|
|
if p.curToken.Type == TokenNewline {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
|
|
// Parse key = value
|
|
keys, err := p.parseKeyParts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.curToken.Type != TokenEqual {
|
|
return nil, fmt.Errorf("expected '=' in inline table at line %d", p.curToken.Line)
|
|
}
|
|
p.nextToken()
|
|
|
|
val, err := p.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Inline tables can have dotted keys too: { a.b = 1 }
|
|
if err := p.assignValue(m, keys, val); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
p.nextToken()
|
|
} else if p.curToken.Type != TokenRBrace {
|
|
if p.curToken.Type == TokenNewline {
|
|
p.nextToken()
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("expected comma or closing brace in inline table at line %d", p.curToken.Line)
|
|
}
|
|
}
|
|
p.nextToken() // consume }
|
|
|
|
// Mark inline table immutable. Value.Pointer for maps is documented
|
|
// stable for identity comparison. Nested inline tables self-mark on return;
|
|
// same-table dotted assignments happen before the mark, so intra-table
|
|
// dotted keys ({a.b = 1}) remain unaffected.
|
|
p.frozen[reflect.ValueOf(m).Pointer()] = true
|
|
return m, nil
|
|
}
|