v0.1.1 decode hardening, round trip support, doc and test update
Test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-28 14:05:12 -04:00
1 parent c8e23ff859
commit b59d068ff6
15 files changed
+874 -651

No files matched your search

+47 -24
View File
@@ -4,11 +4,9 @@ import (
"fmt"
"reflect"
"strconv"
"unicode/utf8"
)
// 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
@@ -49,6 +47,9 @@ func (p *Parser) nextToken() {
}
func (p *Parser) Parse() (map[string]any, error) {
if !utf8.Valid(p.lexer.input) {
return nil, fmt.Errorf("invalid UTF-8")
}
for p.curToken.Type != TokenEOF {
if p.curToken.Type == TokenNewline {
p.nextToken()
@@ -58,6 +59,9 @@ func (p *Parser) Parse() (map[string]any, error) {
if err := p.parseStatement(); err != nil {
return nil, err
}
if p.curToken.Type != TokenNewline && p.curToken.Type != TokenEOF {
return nil, fmt.Errorf("expected newline at line %d", p.curToken.Line)
}
}
return p.root, nil
}
@@ -254,7 +258,7 @@ func (p *Parser) parseKeyParts() ([]string, error) {
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 {
if numericKey(p.curToken.Literal) {
return nil, fmt.Errorf("numeric string keys are forbidden: %q", p.curToken.Literal)
}
}
@@ -263,6 +267,9 @@ func (p *Parser) parseKeyParts() ([]string, error) {
return nil, fmt.Errorf("expected key, got %s", p.curToken.String())
}
if len(keys) >= maxValueDepth {
return nil, fmt.Errorf("key nesting exceeds %d", maxValueDepth)
}
keys = append(keys, p.curToken.Literal)
p.nextToken()
@@ -314,7 +321,7 @@ func (p *Parser) parseValue() (any, error) {
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) {
func (p *Parser) parseInteger(lit string) (int64, error) {
// Handle optional leading sign
negative := false
numLit := lit
@@ -329,26 +336,23 @@ func (p *Parser) parseInteger(lit string) (int, error) {
if len(numLit) > 2 && numLit[0] == '0' {
switch numLit[1] {
case 'x', 'X':
val, err = strconv.ParseInt(numLit[2:], 16, 64)
val, err = strconv.ParseInt(signPrefix(negative)+numLit[2:], 16, 64)
case 'o', 'O':
val, err = strconv.ParseInt(numLit[2:], 8, 64)
val, err = strconv.ParseInt(signPrefix(negative)+numLit[2:], 8, 64)
case 'b', 'B':
val, err = strconv.ParseInt(numLit[2:], 2, 64)
val, err = strconv.ParseInt(signPrefix(negative)+numLit[2:], 2, 64)
default:
val, err = strconv.ParseInt(lit, 10, 64)
return int(val), err
return val, err
}
if err != nil {
return 0, err
}
if negative {
val = -val
}
return int(val), nil
return val, nil
}
val, err = strconv.ParseInt(lit, 10, 64)
return int(val), err
return val, err
}
func (p *Parser) parseArray() ([]any, error) {
@@ -367,15 +371,12 @@ func (p *Parser) parseArray() ([]any, error) {
}
arr = append(arr, val)
for p.curToken.Type == TokenNewline {
p.nextToken()
}
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)
}
}
@@ -414,13 +415,12 @@ func (p *Parser) parseInlineTable() (map[string]any, error) {
return nil, err
}
for p.curToken.Type == TokenNewline {
p.nextToken()
}
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)
}
}
@@ -433,3 +433,26 @@ func (p *Parser) parseInlineTable() (map[string]any, error) {
p.frozen[reflect.ValueOf(m).Pointer()] = true
return m, nil
}
func signPrefix(negative bool) string {
if negative {
return "-"
}
return ""
}
// Keep the package's decimal-key restriction independent of numeric range.
func numericKey(s string) bool {
if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
s = s[1:]
}
if s == "" {
return false
}
for _, c := range s {
if !isDigit(c) {
return false
}
}
return true
}