perf(parse): cut allocations and validate UTF-8 in the scan
This commit is contained in:
@@ -74,6 +74,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- The module path carries the /v2 suffix the Go toolchain requires of
|
- The module path carries the /v2 suffix the Go toolchain requires of
|
||||||
every major version 2 module: imports change to
|
every major version 2 module: imports change to
|
||||||
`sourcedock.dev/petrbalvin/interpres/v2`.
|
`sourcedock.dev/petrbalvin/interpres/v2`.
|
||||||
|
- Input that is not valid UTF-8 is now rejected where the parser's scan
|
||||||
|
meets the invalid byte, with a `SyntaxError` naming that line, instead of
|
||||||
|
a whole-input check that always reported line 1. Invalid input is still
|
||||||
|
rejected; the reported location is now the byte's own.
|
||||||
|
|
||||||
|
**Performance**
|
||||||
|
|
||||||
|
- Parsing is faster than in 1.1.0 while carrying the new document layer:
|
||||||
|
the suite's representative document decodes at about 79 MB/s with 104
|
||||||
|
allocations per call, and the long array-of-tables document at about
|
||||||
|
106 MB/s against 56 MB/s in 1.1.0, with allocations on that document
|
||||||
|
halved from 67 664 to 31 765. Date-time tokens are validated by a byte
|
||||||
|
scan instead of regular expressions, repeated keys share one string
|
||||||
|
across array-of-tables elements, and per-statement buffers are reused.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+25
-6
@@ -24,19 +24,38 @@ func TestSyntaxErrorMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRejectsInvalidUTF8(t *testing.T) {
|
func TestParseRejectsInvalidUTF8(t *testing.T) {
|
||||||
_, err := ParseMap([]byte("v = \"\xff\"\n"))
|
// The scan validates UTF-8 where it meets the byte, so the reported line
|
||||||
|
// is the invalid byte's own, wherever in the document it sits.
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
doc string
|
||||||
|
line int
|
||||||
|
}{
|
||||||
|
{"in a basic string", "v = \"\xff\"\n", 1},
|
||||||
|
{"in a literal string", "v = '\xff'\n", 1},
|
||||||
|
{"in a multiline string", "v = \"\"\"\n\xff\"\"\"\n", 2},
|
||||||
|
{"in a comment", "v = 1\n# caf\xe9\xff\n", 2},
|
||||||
|
{"in a bare key", "va\xfflue = 1\n", 1},
|
||||||
|
{"as a statement", "\xff = 1\n", 1},
|
||||||
|
{"in a bare value", "v = \xff1\n", 1},
|
||||||
|
{"after a value", "v = 1 \xff\n", 1},
|
||||||
|
{"after the first line", "a = 1\nb = \"\xff\"\n", 2},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
_, err := ParseMap([]byte(c.doc))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected a UTF-8 validation error")
|
t.Fatalf("%s: expected a UTF-8 validation error", c.name)
|
||||||
}
|
}
|
||||||
se, ok := err.(*SyntaxError)
|
se, ok := err.(*SyntaxError)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("err is %T, want *SyntaxError", err)
|
t.Fatalf("%s: err is %T, want *SyntaxError", c.name, err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(se.Msg, "UTF-8") {
|
if !strings.Contains(se.Msg, "UTF-8") {
|
||||||
t.Errorf("Msg = %q, want it to mention UTF-8", se.Msg)
|
t.Errorf("%s: Msg = %q, want it to mention UTF-8", c.name, se.Msg)
|
||||||
|
}
|
||||||
|
if se.Line != c.line {
|
||||||
|
t.Errorf("%s: Line = %d, want %d", c.name, se.Line, c.line)
|
||||||
}
|
}
|
||||||
if se.Line != 1 {
|
|
||||||
t.Errorf("Line = %d, want 1", se.Line)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -19,7 +19,8 @@ Decodes a TOML document into a [Document](#documents): the values, the order the
|
|||||||
keys were written in, whether a table was written inline, and the comments.
|
keys were written in, whether a table was written inline, and the comments.
|
||||||
The values follow the mapping in the [Decoding](#decoding) section below.
|
The values follow the mapping in the [Decoding](#decoding) section below.
|
||||||
Returns `*SyntaxError` on a malformed document. Input that is not valid UTF-8
|
Returns `*SyntaxError` on a malformed document. Input that is not valid UTF-8
|
||||||
is rejected before the parser runs. Equivalent to
|
is rejected with a `SyntaxError` naming the line where the invalid byte
|
||||||
|
appears, because validity is checked during the scan. Equivalent to
|
||||||
`ParseContext(context.Background(), data)`.
|
`ParseContext(context.Background(), data)`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
+3
-4
@@ -24,7 +24,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A SyntaxError describes a malformed TOML document, including the 1-based
|
// A SyntaxError describes a malformed TOML document, including the 1-based
|
||||||
@@ -143,9 +142,9 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
|
|||||||
if opts.maxInputSize > 0 && len(data) > opts.maxInputSize {
|
if opts.maxInputSize > 0 && len(data) > opts.maxInputSize {
|
||||||
return nil, nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
|
return nil, nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
|
||||||
}
|
}
|
||||||
if !utf8.Valid(data) {
|
// UTF-8 validity is not checked in a pass of its own: the scanner
|
||||||
return nil, nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"}
|
// validates the multi-byte sequences where it meets them, so an invalid
|
||||||
}
|
// byte is reported on its own line instead of always on line 1.
|
||||||
maxDepth := opts.maxDepth
|
maxDepth := opts.maxDepth
|
||||||
if maxDepth <= 0 {
|
if maxDepth <= 0 {
|
||||||
maxDepth = maxNestingDepth
|
maxDepth = maxNestingDepth
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ func decodeDecimalInt(tok string) (any, error) {
|
|||||||
if err := checkNoLeadingZero(digits); err != nil {
|
if err := checkNoLeadingZero(digits); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// An unsigned token parses in place; only a sign needs the concatenated
|
||||||
|
// copy, and concatenating an empty sign still allocated.
|
||||||
|
if sign == "" {
|
||||||
|
i, err := strconv.ParseInt(digits, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("integer %q out of range", tok)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
i, err := strconv.ParseInt(sign+digits, 10, 64)
|
i, err := strconv.ParseInt(sign+digits, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("integer %q out of range", tok)
|
return nil, fmt.Errorf("integer %q out of range", tok)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ const ctxCheckInterval = 64
|
|||||||
|
|
||||||
// parser is a recursive-descent TOML parser producing a map[string]any tree.
|
// parser is a recursive-descent TOML parser producing a map[string]any tree.
|
||||||
//
|
//
|
||||||
// The scanner works on bytes, not runes: the input is validated UTF-8 before
|
// The scanner works on bytes, not runes: every character that drives the
|
||||||
// the parser runs, every character that drives the grammar (quotes,
|
// grammar (quotes, separators, newlines, bare-key characters) is ASCII, the
|
||||||
// separators, newlines, bare-key characters) is ASCII, and multi-byte runes
|
// scan validates a multi-byte sequence where it meets one, and multi-byte
|
||||||
// matter only as string content, where they are decoded on the spot. Holding
|
// runes matter only as content, where they are decoded on the spot. Holding
|
||||||
// the source as []rune instead would cost a conversion pass plus four bytes
|
// the source as []rune instead would cost a conversion pass plus four bytes
|
||||||
// per rune of extra memory before parsing even starts.
|
// per rune of extra memory before parsing even starts.
|
||||||
type parser struct {
|
type parser struct {
|
||||||
@@ -45,6 +45,21 @@ type parser struct {
|
|||||||
|
|
||||||
currentPath []string
|
currentPath []string
|
||||||
|
|
||||||
|
// keys interns key strings: a document that repeats a key across
|
||||||
|
// array-of-tables elements stores one string per distinct key instead of
|
||||||
|
// one per occurrence. The table is parser-local and dies with the parse;
|
||||||
|
// the tree keeps sharing the strings it was handed.
|
||||||
|
keys map[string]string
|
||||||
|
|
||||||
|
// keyBuf backs the transient single-segment result of parseKeyPath. A
|
||||||
|
// caller that keeps the path copies it out first, which is what
|
||||||
|
// retainPath does for the current section.
|
||||||
|
keyBuf [1]string
|
||||||
|
|
||||||
|
// absScratch backs the absolute path of a top-level key, which lives only
|
||||||
|
// for the statement being parsed.
|
||||||
|
absScratch [1]string
|
||||||
|
|
||||||
// wantDoc asks for the node tree the Document is built from; doc is that
|
// wantDoc asks for the node tree the Document is built from; doc is that
|
||||||
// tree, and it stays nil when only the value tree is wanted. currentNode
|
// tree, and it stays nil when only the value tree is wanted. currentNode
|
||||||
// is the node of p.current; pending collects the comment lines since the
|
// is the node of p.current; pending collects the comment lines since the
|
||||||
@@ -87,10 +102,9 @@ func (p *parser) leaveNesting() { p.depth-- }
|
|||||||
func (p *parser) parse() (map[string]any, error) {
|
func (p *parser) parse() (map[string]any, error) {
|
||||||
p.root = map[string]any{}
|
p.root = map[string]any{}
|
||||||
p.current = p.root
|
p.current = p.root
|
||||||
p.headers = map[string]bool{}
|
// The definition maps start unallocated: a document with no headers, no
|
||||||
p.frozen = map[string]bool{}
|
// dotted keys and no inline tables never pays for them, and a nil map
|
||||||
p.dotted = map[string]bool{}
|
// reads as empty. Each is created on its first write.
|
||||||
p.arrays = map[string]bool{}
|
|
||||||
p.currentPath = nil
|
p.currentPath = nil
|
||||||
if p.wantDoc {
|
if p.wantDoc {
|
||||||
p.doc = newTable(p.root)
|
p.doc = newTable(p.root)
|
||||||
@@ -166,6 +180,56 @@ func (p *parser) checkCtx() error {
|
|||||||
return p.ctx.Err()
|
return p.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- definition maps --------------------------------------------------------
|
||||||
|
|
||||||
|
// The definition maps record what a document has already defined, so a later
|
||||||
|
// statement cannot redefine it. Each is created on first write: reads on a
|
||||||
|
// nil map answer false, which is exactly the state of a map never written.
|
||||||
|
|
||||||
|
func (p *parser) markHeader(pk string) {
|
||||||
|
if p.headers == nil {
|
||||||
|
p.headers = make(map[string]bool, 4)
|
||||||
|
}
|
||||||
|
p.headers[pk] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) markFrozen(pk string) {
|
||||||
|
if p.frozen == nil {
|
||||||
|
p.frozen = make(map[string]bool, 4)
|
||||||
|
}
|
||||||
|
p.frozen[pk] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) markDotted(pk string) {
|
||||||
|
if p.dotted == nil {
|
||||||
|
p.dotted = make(map[string]bool, 4)
|
||||||
|
}
|
||||||
|
p.dotted[pk] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) markArray(pk string) {
|
||||||
|
if p.arrays == nil {
|
||||||
|
p.arrays = make(map[string]bool, 2)
|
||||||
|
}
|
||||||
|
p.arrays[pk] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// internKey returns the shared string for key bytes. The lookup works on the
|
||||||
|
// bytes directly, which the compiler lets run without allocating, so a
|
||||||
|
// repeated key costs no allocation at all and the tree stores one string per
|
||||||
|
// distinct key.
|
||||||
|
func (p *parser) internKey(b []byte) string {
|
||||||
|
if p.keys == nil {
|
||||||
|
p.keys = make(map[string]string, 16)
|
||||||
|
}
|
||||||
|
if s, ok := p.keys[string(b)]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
p.keys[s] = s
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// --- table headers ---------------------------------------------------------
|
// --- table headers ---------------------------------------------------------
|
||||||
|
|
||||||
func (p *parser) parseTableHeader() error {
|
func (p *parser) parseTableHeader() error {
|
||||||
@@ -176,7 +240,7 @@ func (p *parser) parseTableHeader() error {
|
|||||||
p.pos++
|
p.pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := p.parseKeyPath()
|
first, rest, err := p.parseKeyPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -193,6 +257,15 @@ func (p *parser) parseTableHeader() error {
|
|||||||
p.pos++
|
p.pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The key the rest of the header handling reads. parseKeyPath hands back
|
||||||
|
// a transient buffer for the single-segment case, the shape every
|
||||||
|
// repeated array-of-tables header has; anything longer is copied once.
|
||||||
|
key := p.keyBuf[:1]
|
||||||
|
key[0] = first
|
||||||
|
if len(rest) > 0 {
|
||||||
|
key = append([]string{first}, rest...)
|
||||||
|
}
|
||||||
|
|
||||||
if array {
|
if array {
|
||||||
tbl, elem, err := p.appendArrayTable(key)
|
tbl, elem, err := p.appendArrayTable(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,9 +274,9 @@ func (p *parser) parseTableHeader() error {
|
|||||||
// A new array-of-tables element starts a fresh scope: sub-table headers
|
// A new array-of-tables element starts a fresh scope: sub-table headers
|
||||||
// and inline-table freezes from the previous element no longer apply.
|
// and inline-table freezes from the previous element no longer apply.
|
||||||
p.resetScopeUnder(key)
|
p.resetScopeUnder(key)
|
||||||
p.arrays[pathKey(key)] = true
|
p.markArray(pathKey(key))
|
||||||
p.current = tbl
|
p.current = tbl
|
||||||
p.currentPath = key
|
p.currentPath = p.retainPath(key)
|
||||||
p.currentNode = elem
|
p.currentNode = elem
|
||||||
p.lastTable = elem
|
p.lastTable = elem
|
||||||
return nil
|
return nil
|
||||||
@@ -213,30 +286,53 @@ func (p *parser) parseTableHeader() error {
|
|||||||
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
|
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
|
||||||
return p.errf("table %q is defined more than once", strings.Join(key, "."))
|
return p.errf("table %q is defined more than once", strings.Join(key, "."))
|
||||||
}
|
}
|
||||||
p.headers[pk] = true
|
p.markHeader(pk)
|
||||||
|
|
||||||
tbl, node, err := p.tableAt(key)
|
tbl, node, err := p.tableAt(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.current = tbl
|
p.current = tbl
|
||||||
p.currentPath = key
|
p.currentPath = p.retainPath(key)
|
||||||
p.currentNode = node
|
p.currentNode = node
|
||||||
p.lastTable = node
|
p.lastTable = node
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retainPath copies key into the parser-owned storage currentPath holds, so
|
||||||
|
// the transient key buffer is free to serve the next statement.
|
||||||
|
func (p *parser) retainPath(key []string) []string {
|
||||||
|
if cap(p.currentPath) < len(key) {
|
||||||
|
p.currentPath = make([]string, len(key))
|
||||||
|
} else {
|
||||||
|
p.currentPath = p.currentPath[:len(key)]
|
||||||
|
}
|
||||||
|
copy(p.currentPath, key)
|
||||||
|
return p.currentPath
|
||||||
|
}
|
||||||
|
|
||||||
// tableAt walks (creating intermediate tables) to the table named by key,
|
// tableAt walks (creating intermediate tables) to the table named by key,
|
||||||
// relative to the document root, rejecting any step into a frozen inline table.
|
// relative to the document root, rejecting any step into a frozen inline table.
|
||||||
func (p *parser) tableAt(key []string) (map[string]any, *Table, error) {
|
func (p *parser) tableAt(key []string) (map[string]any, *Table, error) {
|
||||||
cur := p.root
|
cur := p.root
|
||||||
node := p.doc
|
node := p.doc
|
||||||
path := make([]string, 0, len(key))
|
// The intermediate-path bookkeeping allocates only when the key actually
|
||||||
|
// has intermediate segments; a single-segment key checks its own name.
|
||||||
|
var path []string
|
||||||
|
if len(key) > 1 {
|
||||||
|
path = make([]string, 0, len(key))
|
||||||
|
}
|
||||||
for _, k := range key {
|
for _, k := range key {
|
||||||
|
if len(key) == 1 {
|
||||||
|
if p.frozen[k] {
|
||||||
|
return nil, nil, p.errf("cannot extend inline table %q", k)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
path = append(path, k)
|
path = append(path, k)
|
||||||
if p.frozen[pathKey(path)] {
|
if p.frozen[pathKey(path)] {
|
||||||
return nil, nil, p.errf("cannot extend inline table %q", strings.Join(path, "."))
|
return nil, nil, p.errf("cannot extend inline table %q", strings.Join(path, "."))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
existing, ok := cur[k]
|
existing, ok := cur[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
next := map[string]any{}
|
next := map[string]any{}
|
||||||
@@ -271,7 +367,12 @@ func (p *parser) tableAt(key []string) (map[string]any, *Table, error) {
|
|||||||
func (p *parser) appendArrayTable(key []string) (map[string]any, *Table, error) {
|
func (p *parser) appendArrayTable(key []string) (map[string]any, *Table, error) {
|
||||||
parent := p.root
|
parent := p.root
|
||||||
node := p.doc
|
node := p.doc
|
||||||
path := make([]string, 0, len(key))
|
// As in tableAt, the path slice exists only for a multi-segment key; the
|
||||||
|
// loop below runs for those alone.
|
||||||
|
var path []string
|
||||||
|
if len(key) > 1 {
|
||||||
|
path = make([]string, 0, len(key))
|
||||||
|
}
|
||||||
for _, k := range key[:len(key)-1] {
|
for _, k := range key[:len(key)-1] {
|
||||||
path = append(path, k)
|
path = append(path, k)
|
||||||
if p.frozen[pathKey(path)] {
|
if p.frozen[pathKey(path)] {
|
||||||
@@ -323,7 +424,7 @@ func (p *parser) appendArrayTable(key []string) (map[string]any, *Table, error)
|
|||||||
// --- key/value -------------------------------------------------------------
|
// --- key/value -------------------------------------------------------------
|
||||||
|
|
||||||
func (p *parser) parseKeyValue() error {
|
func (p *parser) parseKeyValue() error {
|
||||||
key, err := p.parseKeyPath()
|
first, rest, err := p.parseKeyPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,47 +441,46 @@ func (p *parser) parseKeyValue() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dest := p.current
|
dest := p.current
|
||||||
// One allocation covers the current section plus the dotted key; a
|
// The absolute path of the key drives the dotted-key bookkeeping and the
|
||||||
// top-level statement reuses it for the leaf.
|
// inline-table freeze. A single top-level key needs it only for the
|
||||||
abs := make([]string, 0, len(p.currentPath)+len(key))
|
// freeze, where a one-element path sits in the parser's scratch.
|
||||||
|
var abs []string
|
||||||
|
if len(rest) > 0 || len(p.currentPath) > 0 {
|
||||||
|
abs = make([]string, 0, len(p.currentPath)+len(rest)+1)
|
||||||
abs = append(abs, p.currentPath...)
|
abs = append(abs, p.currentPath...)
|
||||||
|
abs = append(abs, first)
|
||||||
|
} else {
|
||||||
|
abs = append(p.absScratch[:0], first)
|
||||||
|
}
|
||||||
|
|
||||||
// dests collects the map each dotted key descended into, which the node
|
// dests collects the map each dotted key descended into, which the node
|
||||||
// tree needs to build the matching tables around the value.
|
// tree needs to build the matching tables around the value.
|
||||||
var dests []map[string]any
|
var dests []map[string]any
|
||||||
for _, k := range key[:len(key)-1] {
|
leaf := first
|
||||||
|
if len(rest) > 0 {
|
||||||
|
if err := p.descendKey(&dest, first, abs, &dests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, k := range rest[:len(rest)-1] {
|
||||||
abs = append(abs, k)
|
abs = append(abs, k)
|
||||||
if p.frozen[pathKey(abs)] {
|
if err := p.descendKey(&dest, k, abs, &dests); err != nil {
|
||||||
return p.errf("cannot extend inline table %q", strings.Join(abs, "."))
|
return err
|
||||||
}
|
}
|
||||||
if p.headers[pathKey(abs)] {
|
|
||||||
return p.errf("cannot extend table %q with a dotted key", strings.Join(abs, "."))
|
|
||||||
}
|
}
|
||||||
p.dotted[pathKey(abs)] = true
|
leaf = rest[len(rest)-1]
|
||||||
existing, ok := dest[k]
|
|
||||||
if !ok {
|
|
||||||
next := map[string]any{}
|
|
||||||
dest[k] = next
|
|
||||||
dest = next
|
|
||||||
dests = append(dests, next)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m, ok := existing.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return p.errf("key %q is not a table", k)
|
|
||||||
}
|
|
||||||
dest = m
|
|
||||||
dests = append(dests, m)
|
|
||||||
}
|
|
||||||
leaf := key[len(key)-1]
|
|
||||||
abs = append(abs, leaf)
|
abs = append(abs, leaf)
|
||||||
|
}
|
||||||
if _, exists := dest[leaf]; exists {
|
if _, exists := dest[leaf]; exists {
|
||||||
return p.errf("duplicate key %q", leaf)
|
return p.errf("duplicate key %q", leaf)
|
||||||
}
|
}
|
||||||
dest[leaf] = val
|
dest[leaf] = val
|
||||||
if p.doc != nil {
|
if p.doc != nil {
|
||||||
node := p.currentNode
|
node := p.currentNode
|
||||||
for i, k := range key[:len(key)-1] {
|
if len(rest) > 0 {
|
||||||
node = node.addTable(k, dests[i])
|
node = node.addTable(first, dests[0])
|
||||||
|
for i, k := range rest[:len(rest)-1] {
|
||||||
|
node = node.addTable(k, dests[i+1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, inline := val.(map[string]any)
|
_, inline := val.(map[string]any)
|
||||||
entry := node.addValue(leaf, val, inline)
|
entry := node.addValue(leaf, val, inline)
|
||||||
@@ -396,6 +496,35 @@ func (p *parser) parseKeyValue() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// descendKey walks dest into the sub-table named key on the dotted path abs,
|
||||||
|
// recording the path in the definition maps; dests collects the maps
|
||||||
|
// descended into.
|
||||||
|
func (p *parser) descendKey(dest *map[string]any, key string, abs []string, dests *[]map[string]any) error {
|
||||||
|
ak := pathKey(abs)
|
||||||
|
if p.frozen[ak] {
|
||||||
|
return p.errf("cannot extend inline table %q", strings.Join(abs, "."))
|
||||||
|
}
|
||||||
|
if p.headers[ak] {
|
||||||
|
return p.errf("cannot extend table %q with a dotted key", strings.Join(abs, "."))
|
||||||
|
}
|
||||||
|
p.markDotted(ak)
|
||||||
|
existing, ok := (*dest)[key]
|
||||||
|
if !ok {
|
||||||
|
next := map[string]any{}
|
||||||
|
(*dest)[key] = next
|
||||||
|
*dest = next
|
||||||
|
*dests = append(*dests, next)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m, ok := existing.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return p.errf("key %q is not a table", key)
|
||||||
|
}
|
||||||
|
*dest = m
|
||||||
|
*dests = append(*dests, m)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// takeInline returns the node of the inline table just parsed, when v is that
|
// takeInline returns the node of the inline table just parsed, when v is that
|
||||||
// table's value, and clears it so a later value cannot pick it up.
|
// table's value, and clears it so a later value cannot pick it up.
|
||||||
func (p *parser) takeInline(v any) *Table {
|
func (p *parser) takeInline(v any) *Table {
|
||||||
@@ -419,16 +548,17 @@ func (p *parser) takeArrayElems(v any) []*Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// freezeInline marks the path of an inline table (and any nested inline tables)
|
// 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.
|
// as immutable, so a later header or dotted key cannot extend it. The
|
||||||
|
// recursion appends into the caller's path slice; the frozen map keeps the
|
||||||
|
// joined strings, never the slice, so the backing is free to be reused.
|
||||||
func (p *parser) freezeInline(path []string, val any) {
|
func (p *parser) freezeInline(path []string, val any) {
|
||||||
m, ok := val.(map[string]any)
|
m, ok := val.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.frozen[pathKey(path)] = true
|
p.markFrozen(pathKey(path))
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
child := append(append([]string{}, path...), k)
|
p.freezeInline(append(path, k), v)
|
||||||
p.freezeInline(child, v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,33 +568,54 @@ func (p *parser) freezeInline(path []string, val any) {
|
|||||||
// fresh in the new element.
|
// fresh in the new element.
|
||||||
func (p *parser) resetScopeUnder(key []string) {
|
func (p *parser) resetScopeUnder(key []string) {
|
||||||
prefix := pathKey(key) + "\x00"
|
prefix := pathKey(key) + "\x00"
|
||||||
for _, m := range []map[string]bool{p.headers, p.frozen, p.dotted, p.arrays} {
|
p.resetMapUnder(p.headers, prefix)
|
||||||
|
p.resetMapUnder(p.frozen, prefix)
|
||||||
|
p.resetMapUnder(p.dotted, prefix)
|
||||||
|
p.resetMapUnder(p.arrays, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetMapUnder deletes the entries m holds under prefix. An empty or
|
||||||
|
// unallocated map holds none, so the common case walks nothing.
|
||||||
|
func (p *parser) resetMapUnder(m map[string]bool, prefix string) {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
for k := range m {
|
for k := range m {
|
||||||
if strings.HasPrefix(k, prefix) {
|
if strings.HasPrefix(k, prefix) {
|
||||||
delete(m, k)
|
delete(m, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// parseKeyPath parses a dotted key into its components.
|
// parseKeyPath parses a dotted key. The first component comes back directly
|
||||||
func (p *parser) parseKeyPath() ([]string, error) {
|
// and the rest as a usually nil slice, because a single-component key is the
|
||||||
var parts []string
|
// common shape and a fresh slice per statement is what the allocation profile
|
||||||
|
// showed. The single-key slice a caller sees is parser-owned and transient.
|
||||||
|
func (p *parser) parseKeyPath() (string, []string, error) {
|
||||||
|
p.skipInline()
|
||||||
|
first, err := p.parseKeyComponent()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
p.skipInline()
|
||||||
|
if p.eof() || p.peek() != '.' {
|
||||||
|
return first, nil, nil
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
var rest []string
|
||||||
for {
|
for {
|
||||||
p.skipInline()
|
p.skipInline()
|
||||||
part, err := p.parseKeyComponent()
|
part, err := p.parseKeyComponent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
parts = append(parts, part)
|
rest = append(rest, part)
|
||||||
p.skipInline()
|
p.skipInline()
|
||||||
if !p.eof() && p.peek() == '.' {
|
if p.eof() || p.peek() != '.' {
|
||||||
|
return first, rest, nil
|
||||||
|
}
|
||||||
p.pos++
|
p.pos++
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
return parts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseKeyComponent() (string, error) {
|
func (p *parser) parseKeyComponent() (string, error) {
|
||||||
@@ -493,11 +644,18 @@ func (p *parser) parseKeyComponent() (string, error) {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// The stopping byte decides the message: a multi-byte sequence that
|
||||||
|
// does not decode names that, before any grammar message can.
|
||||||
|
if !p.eof() && p.peek() >= utf8.RuneSelf {
|
||||||
|
if r, size := utf8.DecodeRune(p.src[p.pos:]); r == utf8.RuneError && size == 1 {
|
||||||
|
return "", p.errf("invalid UTF-8 in key")
|
||||||
|
}
|
||||||
|
}
|
||||||
if p.pos == start {
|
if p.pos == start {
|
||||||
r, _ := utf8.DecodeRune(p.src[p.pos:])
|
r, _ := utf8.DecodeRune(p.src[p.pos:])
|
||||||
return "", p.errf("invalid key character %q", string(r))
|
return "", p.errf("invalid key character %q", string(r))
|
||||||
}
|
}
|
||||||
return string(p.src[start:p.pos]), nil
|
return p.internKey(p.src[start:p.pos]), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +702,9 @@ func (p *parser) parseAtom() (any, error) {
|
|||||||
if tok == "" {
|
if tok == "" {
|
||||||
return nil, p.errf("expected a value")
|
return nil, p.errf("expected a value")
|
||||||
}
|
}
|
||||||
|
if hasHighByte(tok) && !utf8.ValidString(tok) {
|
||||||
|
return nil, p.errf("invalid UTF-8 in value")
|
||||||
|
}
|
||||||
// A date may be followed by a space and a time, forming one date-time.
|
// A date may be followed by a space and a time, forming one date-time.
|
||||||
if isDateToken(tok) && !p.eof() && p.peek() == ' ' {
|
if isDateToken(tok) && !p.eof() && p.peek() == ' ' {
|
||||||
if next, ok := p.peekAt(1); ok && next >= '0' && next <= '9' {
|
if next, ok := p.peekAt(1); ok && next >= '0' && next <= '9' {
|
||||||
@@ -576,6 +737,17 @@ func (p *parser) scanBareToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasHighByte reports whether s holds any byte outside ASCII, the cheap gate
|
||||||
|
// in front of a full UTF-8 check.
|
||||||
|
func hasHighByte(s string) bool {
|
||||||
|
for i := range len(s) {
|
||||||
|
if s[i] >= utf8.RuneSelf {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// --- strings ---------------------------------------------------------------
|
// --- strings ---------------------------------------------------------------
|
||||||
|
|
||||||
func (p *parser) parseBasicString() (string, error) {
|
func (p *parser) parseBasicString() (string, error) {
|
||||||
@@ -583,7 +755,36 @@ func (p *parser) parseBasicString() (string, error) {
|
|||||||
return p.parseMultilineString('"', true)
|
return p.parseMultilineString('"', true)
|
||||||
}
|
}
|
||||||
p.pos++ // opening quote
|
p.pos++ // opening quote
|
||||||
|
start := p.pos
|
||||||
|
// A run of plain characters up to the closing quote needs no builder, only
|
||||||
|
// one copy at the end; escapes, controls and multi-byte runes fall through
|
||||||
|
// to the builder loop, which validates them on the spot.
|
||||||
|
for p.pos < len(p.src) {
|
||||||
|
c := p.src[p.pos]
|
||||||
|
if c == '"' {
|
||||||
|
s := string(p.src[start:p.pos])
|
||||||
|
p.pos++
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
if c == '\\' || c == '\n' || c == '\r' || c >= utf8.RuneSelf ||
|
||||||
|
(c < 0x20 && c != '\t') || c == 0x7f {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
}
|
||||||
|
if p.eof() {
|
||||||
|
return "", p.errf("unterminated string")
|
||||||
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
b.Grow(p.pos - start)
|
||||||
|
b.Write(p.src[start:p.pos])
|
||||||
|
return p.parseBasicStringRest(&b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBasicStringRest continues a basic string whose fast scan has met a byte
|
||||||
|
// it does not handle: an escape, a control character, a multi-byte rune, or a
|
||||||
|
// bare newline, which the loop rejects.
|
||||||
|
func (p *parser) parseBasicStringRest(b *strings.Builder) (string, error) {
|
||||||
for {
|
for {
|
||||||
if p.eof() {
|
if p.eof() {
|
||||||
return "", p.errf("unterminated string")
|
return "", p.errf("unterminated string")
|
||||||
@@ -605,7 +806,7 @@ func (p *parser) parseBasicString() (string, error) {
|
|||||||
}
|
}
|
||||||
b.WriteRune(r)
|
b.WriteRune(r)
|
||||||
default:
|
default:
|
||||||
if err := p.writeContentRune(&b); err != nil {
|
if err := p.writeContentRune(b); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,7 +818,27 @@ func (p *parser) parseLiteralString() (string, error) {
|
|||||||
return p.parseMultilineString('\'', false)
|
return p.parseMultilineString('\'', false)
|
||||||
}
|
}
|
||||||
p.pos++ // opening quote
|
p.pos++ // opening quote
|
||||||
|
start := p.pos
|
||||||
|
// The same fast scan as the basic string, without the escape case.
|
||||||
|
for p.pos < len(p.src) {
|
||||||
|
c := p.src[p.pos]
|
||||||
|
if c == '\'' {
|
||||||
|
s := string(p.src[start:p.pos])
|
||||||
|
p.pos++
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
if c == '\n' || c == '\r' || c >= utf8.RuneSelf ||
|
||||||
|
(c < 0x20 && c != '\t') || c == 0x7f {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
}
|
||||||
|
if p.eof() {
|
||||||
|
return "", p.errf("unterminated literal string")
|
||||||
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
b.Grow(p.pos - start)
|
||||||
|
b.Write(p.src[start:p.pos])
|
||||||
for {
|
for {
|
||||||
if p.eof() {
|
if p.eof() {
|
||||||
return "", p.errf("unterminated literal string")
|
return "", p.errf("unterminated literal string")
|
||||||
@@ -641,8 +862,8 @@ func (p *parser) parseLiteralString() (string, error) {
|
|||||||
|
|
||||||
// writeContentRune appends the rune at the cursor to b and advances past it.
|
// writeContentRune appends the rune at the cursor to b and advances past it.
|
||||||
// An ASCII byte, which includes every control character the grammar forbids,
|
// An ASCII byte, which includes every control character the grammar forbids,
|
||||||
// is checked and written directly; a multi-byte rune is decoded and can never
|
// is checked and written directly; a multi-byte rune is decoded, and a
|
||||||
// be a control character.
|
// sequence that does not decode is the UTF-8 error reported where it sits.
|
||||||
func (p *parser) writeContentRune(b *strings.Builder) error {
|
func (p *parser) writeContentRune(b *strings.Builder) error {
|
||||||
c := p.peek()
|
c := p.peek()
|
||||||
if c < utf8.RuneSelf {
|
if c < utf8.RuneSelf {
|
||||||
@@ -654,6 +875,9 @@ func (p *parser) writeContentRune(b *strings.Builder) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r, size := utf8.DecodeRune(p.src[p.pos:])
|
r, size := utf8.DecodeRune(p.src[p.pos:])
|
||||||
|
if r == utf8.RuneError && size == 1 {
|
||||||
|
return p.errf("invalid UTF-8 in string")
|
||||||
|
}
|
||||||
p.pos += size
|
p.pos += size
|
||||||
b.WriteRune(r)
|
b.WriteRune(r)
|
||||||
return nil
|
return nil
|
||||||
@@ -831,7 +1055,10 @@ func (p *parser) parseArray() (val any, err error) {
|
|||||||
}
|
}
|
||||||
defer p.leaveNesting()
|
defer p.leaveNesting()
|
||||||
p.pos++ // '['
|
p.pos++ // '['
|
||||||
arr := []any{}
|
// A small presize covers the arrays documents actually hold, and trades a
|
||||||
|
// little capacity on tiny arrays for the growth chain an append-from-nil
|
||||||
|
// costs per array.
|
||||||
|
arr := make([]any, 0, 4)
|
||||||
// elems carries the node of each element that is an inline table, so the
|
// elems carries the node of each element that is an inline table, so the
|
||||||
// caller can keep its key order; the entries are nil for other values.
|
// caller can keep its key order; the entries are nil for other values.
|
||||||
var elems []*Table
|
var elems []*Table
|
||||||
@@ -886,7 +1113,9 @@ func (p *parser) parseInlineTable() (val any, err error) {
|
|||||||
defer p.leaveNesting()
|
defer p.leaveNesting()
|
||||||
p.pos++ // '{'
|
p.pos++ // '{'
|
||||||
tbl := map[string]any{}
|
tbl := map[string]any{}
|
||||||
assigned := map[string]bool{}
|
// assigned tracks the dotted paths written into this table. It is created
|
||||||
|
// on the first key, so an empty inline table allocates nothing for it.
|
||||||
|
var assigned map[string]bool
|
||||||
// The inline table is a node of its own, so the keys keep their order; the
|
// The inline table is a node of its own, so the keys keep their order; the
|
||||||
// caller picks the node up when the table parses.
|
// caller picks the node up when the table parses.
|
||||||
var node *Table
|
var node *Table
|
||||||
@@ -913,7 +1142,7 @@ func (p *parser) parseInlineTable() (val any, err error) {
|
|||||||
if err := p.skipNestedSpace(); err != nil {
|
if err := p.skipNestedSpace(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
key, err := p.parseKeyPath()
|
first, rest, err := p.parseKeyPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -929,39 +1158,46 @@ func (p *parser) parseInlineTable() (val any, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dest := tbl
|
dest := tbl
|
||||||
path := make([]string, 0, len(key))
|
var path []string
|
||||||
var dests []map[string]any
|
var dests []map[string]any
|
||||||
for _, k := range key[:len(key)-1] {
|
leaf := first
|
||||||
|
if len(rest) > 0 {
|
||||||
|
path = append(p.absScratch[:0], first)
|
||||||
|
d, err := p.descendInline(&dest, first, path, assigned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dests = append(dests, d)
|
||||||
|
for _, k := range rest[:len(rest)-1] {
|
||||||
path = append(path, k)
|
path = append(path, k)
|
||||||
if assigned[pathKey(path)] {
|
d, err := p.descendInline(&dest, k, path, assigned)
|
||||||
return nil, p.errf("key %q is already defined", strings.Join(path, "."))
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
existing, ok := dest[k]
|
dests = append(dests, d)
|
||||||
if !ok {
|
|
||||||
m := map[string]any{}
|
|
||||||
dest[k] = m
|
|
||||||
dest = m
|
|
||||||
dests = append(dests, m)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
m, isMap := existing.(map[string]any)
|
leaf = rest[len(rest)-1]
|
||||||
if !isMap {
|
|
||||||
return nil, p.errf("key %q is already defined", k)
|
|
||||||
}
|
|
||||||
dest = m
|
|
||||||
dests = append(dests, m)
|
|
||||||
}
|
|
||||||
leaf := key[len(key)-1]
|
|
||||||
path = append(path, leaf)
|
path = append(path, leaf)
|
||||||
|
}
|
||||||
if _, exists := dest[leaf]; exists {
|
if _, exists := dest[leaf]; exists {
|
||||||
return nil, p.errf("duplicate key %q in inline table", leaf)
|
return nil, p.errf("duplicate key %q in inline table", leaf)
|
||||||
}
|
}
|
||||||
dest[leaf] = val
|
dest[leaf] = val
|
||||||
|
if assigned == nil {
|
||||||
|
assigned = make(map[string]bool, 4)
|
||||||
|
}
|
||||||
|
if len(rest) == 0 {
|
||||||
|
assigned[first] = true
|
||||||
|
} else {
|
||||||
assigned[pathKey(path)] = true
|
assigned[pathKey(path)] = true
|
||||||
|
}
|
||||||
if node != nil {
|
if node != nil {
|
||||||
child := node
|
child := node
|
||||||
for i, k := range key[:len(key)-1] {
|
if len(rest) > 0 {
|
||||||
child = child.addTable(k, dests[i])
|
child = child.addTable(first, dests[0])
|
||||||
|
for i, k := range rest[:len(rest)-1] {
|
||||||
|
child = child.addTable(k, dests[i+1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, inline := val.(map[string]any)
|
_, inline := val.(map[string]any)
|
||||||
entry := child.addValue(leaf, val, inline)
|
entry := child.addValue(leaf, val, inline)
|
||||||
@@ -998,6 +1234,28 @@ func (p *parser) parseInlineTable() (val any, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// descendInline walks dest into the sub-table named key inside an inline
|
||||||
|
// table, rejecting a dotted segment the table has already defined.
|
||||||
|
func (p *parser) descendInline(dest *map[string]any, key string, path []string, assigned map[string]bool) (map[string]any, error) {
|
||||||
|
pk := pathKey(path)
|
||||||
|
if assigned[pk] {
|
||||||
|
return nil, p.errf("key %q is already defined", strings.Join(path, "."))
|
||||||
|
}
|
||||||
|
existing, ok := (*dest)[key]
|
||||||
|
if !ok {
|
||||||
|
m := map[string]any{}
|
||||||
|
(*dest)[key] = m
|
||||||
|
*dest = m
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m, isMap := existing.(map[string]any)
|
||||||
|
if !isMap {
|
||||||
|
return nil, p.errf("key %q is already defined", key)
|
||||||
|
}
|
||||||
|
*dest = m
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- scanning helpers ------------------------------------------------------
|
// --- scanning helpers ------------------------------------------------------
|
||||||
|
|
||||||
func (p *parser) eof() bool { return p.pos >= len(p.src) }
|
func (p *parser) eof() bool { return p.pos >= len(p.src) }
|
||||||
@@ -1121,8 +1379,14 @@ func (p *parser) skipComment() (string, error) {
|
|||||||
p.pos++
|
p.pos++
|
||||||
case c < 0x20 || c == 0x7f:
|
case c < 0x20 || c == 0x7f:
|
||||||
return "", p.errf("control character U+%04X is not allowed in a comment", c)
|
return "", p.errf("control character U+%04X is not allowed in a comment", c)
|
||||||
default:
|
case c < utf8.RuneSelf:
|
||||||
p.pos++
|
p.pos++
|
||||||
|
default:
|
||||||
|
r, size := utf8.DecodeRune(p.src[p.pos:])
|
||||||
|
if r == utf8.RuneError && size == 1 {
|
||||||
|
return "", p.errf("invalid UTF-8 in comment")
|
||||||
|
}
|
||||||
|
p.pos += size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return commentText(string(p.src[start:p.pos])), nil
|
return commentText(string(p.src[start:p.pos])), nil
|
||||||
@@ -1176,7 +1440,10 @@ func (p *parser) expectLineEnd() error {
|
|||||||
p.pos++
|
p.pos++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r, _ := utf8.DecodeRune(p.src[p.pos:])
|
r, size := utf8.DecodeRune(p.src[p.pos:])
|
||||||
|
if r == utf8.RuneError && size == 1 {
|
||||||
|
return p.errf("invalid UTF-8 after value")
|
||||||
|
}
|
||||||
return p.errf("unexpected %q after value", string(r))
|
return p.errf("unexpected %q after value", string(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,7 +1452,8 @@ func (p *parser) errf(format string, args ...any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pathKey joins key components with a NUL separator so a dotted path can be
|
// pathKey joins key components with a NUL separator so a dotted path can be
|
||||||
// used as a map key for tracking defined tables.
|
// used as a map key for tracking defined tables. A single component comes
|
||||||
|
// back as it is, with no join and no copy.
|
||||||
func pathKey(parts []string) string {
|
func pathKey(parts []string) string {
|
||||||
return strings.Join(parts, "\x00")
|
return strings.Join(parts, "\x00")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user