mirror of
https://github.com/protocolbuffers/protobuf-go.git
synced 2025-02-04 03:39:48 +00:00
9b3d97c473
* Fixes golang/protobuf#842. Unmarshal can now parse singular or repeated message fields without the field separator. * Fixes golang/protobuf#1011. Handles negative 0 properly. * For unknown fields with fixed 32-bit and 64-bit wire types, output is now in hex format with 0x prefix similar to C++ lib output. Previous Go implementation simply outputs these as decimal numbers %d. * All parsing errors, except for unexpected EOF should now contain line and column number info. * Fixed following conformance-related features: * Parse nan,inf,-inf,infinity,-infinity as case-insensitive. * Interpret float32 overflows as inf or -inf. * Parse large int-like number as proto float. * Discard unknown map field if DiscardUnknown=true. * Allow whitespaces/comments in Any type URL and extension field names per spec. * Improves performance and memory usage. It is now as fast and efficient as protojson, if not better on most benchmarks. name old time/op new time/op delta Text/Unmarshal/google_message1_proto2-4 14.1µs ±43% 8.7µs ±12% -38.27% (p=0.000 n=10+10) Text/Unmarshal/google_message1_proto3-4 11.6µs ±18% 7.7µs ± 9% -33.69% (p=0.000 n=10+10) Text/Unmarshal/google_message2-4 6.20ms ±27% 4.10ms ± 5% -33.95% (p=0.000 n=10+10) Text/Marshal/google_message1_proto2-4 12.8µs ± 6% 10.3µs ±23% -19.54% (p=0.000 n=9+10) Text/Marshal/google_message1_proto3-4 11.9µs ±16% 8.6µs ±10% -27.45% (p=0.000 n=10+10) Text/Marshal/google_message2-4 5.59ms ± 5% 5.30ms ±22% ~ (p=0.356 n=9+10) JSON/Unmarshal/google_message1_proto2-4 12.3µs ±61% 13.9µs ±26% ~ (p=0.190 n=10+10) JSON/Unmarshal/google_message1_proto3-4 7.51µs ± 6% 7.86µs ± 1% +4.66% (p=0.010 n=10+9) JSON/Unmarshal/google_message2-4 3.74ms ± 2% 3.94ms ± 2% +5.32% (p=0.000 n=10+10) JSON/Marshal/google_message1_proto2-4 9.90µs ±12% 9.95µs ± 4% ~ (p=0.315 n=9+10) JSON/Marshal/google_message1_proto3-4 7.55µs ± 4% 7.93µs ± 3% +4.98% (p=0.000 n=10+10) JSON/Marshal/google_message2-4 4.29ms ± 5% 4.49ms ± 2% +4.53% (p=0.001 n=10+10) name old alloc/op new alloc/op delta Text/Unmarshal/google_message1_proto2-4 12.5kB ± 0% 2.0kB ± 0% -83.87% (p=0.000 n=10+10) Text/Unmarshal/google_message1_proto3-4 12.2kB ± 0% 1.8kB ± 0% -85.33% (p=0.000 n=10+10) Text/Unmarshal/google_message2-4 5.35MB ± 0% 0.89MB ± 0% -83.28% (p=0.000 n=10+9) Text/Marshal/google_message1_proto2-4 12.0kB ± 0% 1.4kB ± 0% -88.15% (p=0.000 n=10+10) Text/Marshal/google_message1_proto3-4 12.4kB ± 0% 1.9kB ± 0% -84.91% (p=0.000 n=10+10) Text/Marshal/google_message2-4 5.64MB ± 0% 1.02MB ± 0% -81.85% (p=0.000 n=10+9) JSON/Unmarshal/google_message1_proto2-4 2.29kB ± 0% 2.29kB ± 0% ~ (all equal) JSON/Unmarshal/google_message1_proto3-4 2.08kB ± 0% 2.08kB ± 0% ~ (all equal) JSON/Unmarshal/google_message2-4 899kB ± 0% 899kB ± 0% ~ (p=1.000 n=10+10) JSON/Marshal/google_message1_proto2-4 1.46kB ± 0% 1.46kB ± 0% ~ (all equal) JSON/Marshal/google_message1_proto3-4 1.36kB ± 0% 1.36kB ± 0% ~ (all equal) JSON/Marshal/google_message2-4 1.19MB ± 0% 1.19MB ± 0% ~ (p=0.197 n=10+10) name old allocs/op new allocs/op delta Text/Unmarshal/google_message1_proto2-4 133 ± 0% 89 ± 0% -33.08% (p=0.000 n=10+10) Text/Unmarshal/google_message1_proto3-4 108 ± 0% 67 ± 0% -37.96% (p=0.000 n=10+10) Text/Unmarshal/google_message2-4 60.0k ± 0% 38.7k ± 0% -35.52% (p=0.000 n=10+10) Text/Marshal/google_message1_proto2-4 65.0 ± 0% 25.0 ± 0% -61.54% (p=0.000 n=10+10) Text/Marshal/google_message1_proto3-4 59.0 ± 0% 22.0 ± 0% -62.71% (p=0.000 n=10+10) Text/Marshal/google_message2-4 27.4k ± 0% 7.3k ± 0% -73.39% (p=0.000 n=10+10) JSON/Unmarshal/google_message1_proto2-4 95.0 ± 0% 95.0 ± 0% ~ (all equal) JSON/Unmarshal/google_message1_proto3-4 74.0 ± 0% 74.0 ± 0% ~ (all equal) JSON/Unmarshal/google_message2-4 36.3k ± 0% 36.3k ± 0% ~ (all equal) JSON/Marshal/google_message1_proto2-4 27.0 ± 0% 27.0 ± 0% ~ (all equal) JSON/Marshal/google_message1_proto3-4 30.0 ± 0% 30.0 ± 0% ~ (all equal) JSON/Marshal/google_message2-4 11.3k ± 0% 11.3k ± 0% ~ (p=1.000 n=10+10) Change-Id: I377925facde5535f06333b6f25e9c9b358dc062f Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/204602 Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
660 lines
16 KiB
Go
660 lines
16 KiB
Go
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package text
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
|
|
"google.golang.org/protobuf/internal/errors"
|
|
)
|
|
|
|
// Decoder is a token-based textproto decoder.
|
|
type Decoder struct {
|
|
// lastCall is last method called, either readCall or peekCall.
|
|
// Initial value is readCall.
|
|
lastCall call
|
|
|
|
// lastToken contains the last read token.
|
|
lastToken Token
|
|
|
|
// lastErr contains the last read error.
|
|
lastErr error
|
|
|
|
// openStack is a stack containing the byte characters for MessageOpen and
|
|
// ListOpen kinds. The top of stack represents the message or the list that
|
|
// the current token is nested in. An empty stack means the current token is
|
|
// at the top level message. The characters '{' and '<' both represent the
|
|
// MessageOpen kind.
|
|
openStack []byte
|
|
|
|
// orig is used in reporting line and column.
|
|
orig []byte
|
|
// in contains the unconsumed input.
|
|
in []byte
|
|
}
|
|
|
|
// NewDecoder returns a Decoder to read the given []byte.
|
|
func NewDecoder(b []byte) *Decoder {
|
|
return &Decoder{orig: b, in: b}
|
|
}
|
|
|
|
// call specifies which Decoder method was invoked.
|
|
type call uint8
|
|
|
|
const (
|
|
readCall call = iota
|
|
peekCall
|
|
)
|
|
|
|
// Peek looks ahead and returns the next token and error without advancing a read.
|
|
func (d *Decoder) Peek() (Token, error) {
|
|
defer func() { d.lastCall = peekCall }()
|
|
if d.lastCall == readCall {
|
|
d.lastToken, d.lastErr = d.Read()
|
|
}
|
|
return d.lastToken, d.lastErr
|
|
}
|
|
|
|
// Read returns the next token.
|
|
// It will return an error if there is no valid token.
|
|
func (d *Decoder) Read() (Token, error) {
|
|
defer func() { d.lastCall = readCall }()
|
|
if d.lastCall == peekCall {
|
|
return d.lastToken, d.lastErr
|
|
}
|
|
|
|
tok, err := d.parseNext(d.lastToken.Kind())
|
|
if err != nil {
|
|
return Token{}, err
|
|
}
|
|
|
|
switch tok.kind {
|
|
case comma, semicolon:
|
|
tok, err = d.parseNext(tok.kind)
|
|
if err != nil {
|
|
return Token{}, err
|
|
}
|
|
}
|
|
d.lastToken = tok
|
|
return tok, nil
|
|
}
|
|
|
|
const (
|
|
mismatchedFmt = "mismatched close character %q"
|
|
unexpectedFmt = "unexpected character %q"
|
|
)
|
|
|
|
// parseNext parses the next Token based on given last kind.
|
|
func (d *Decoder) parseNext(lastKind Kind) (Token, error) {
|
|
// Trim leading spaces.
|
|
d.consume(0)
|
|
isEOF := false
|
|
if len(d.in) == 0 {
|
|
isEOF = true
|
|
}
|
|
|
|
switch lastKind {
|
|
case EOF:
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
|
|
case bof:
|
|
// Start of top level message. Next token can be EOF or Name.
|
|
if isEOF {
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
}
|
|
return d.parseFieldName()
|
|
|
|
case Name:
|
|
// Next token can be MessageOpen, ListOpen or Scalar.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case '{', '<':
|
|
d.pushOpenStack(ch)
|
|
return d.consumeToken(MessageOpen, 1, 0), nil
|
|
case '[':
|
|
d.pushOpenStack(ch)
|
|
return d.consumeToken(ListOpen, 1, 0), nil
|
|
default:
|
|
return d.parseScalar()
|
|
}
|
|
|
|
case Scalar:
|
|
openKind, closeCh := d.currentOpenKind()
|
|
switch openKind {
|
|
case bof:
|
|
// Top level message.
|
|
// Next token can be EOF, comma, semicolon or Name.
|
|
if isEOF {
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
}
|
|
switch d.in[0] {
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case MessageOpen:
|
|
// Next token can be MessageClose, comma, semicolon or Name.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(MessageClose, 1, 0), nil
|
|
case otherCloseChar[closeCh]:
|
|
return Token{}, d.newSyntaxError(mismatchedFmt, ch)
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case ListOpen:
|
|
// Next token can be ListClose or comma.
|
|
switch ch := d.in[0]; ch {
|
|
case ']':
|
|
d.popOpenStack()
|
|
return d.consumeToken(ListClose, 1, 0), nil
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
default:
|
|
return Token{}, d.newSyntaxError(unexpectedFmt, ch)
|
|
}
|
|
}
|
|
|
|
case MessageOpen:
|
|
// Next token can be MessageClose or Name.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
_, closeCh := d.currentOpenKind()
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(MessageClose, 1, 0), nil
|
|
case otherCloseChar[closeCh]:
|
|
return Token{}, d.newSyntaxError(mismatchedFmt, ch)
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case MessageClose:
|
|
openKind, closeCh := d.currentOpenKind()
|
|
switch openKind {
|
|
case bof:
|
|
// Top level message.
|
|
// Next token can be EOF, comma, semicolon or Name.
|
|
if isEOF {
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case MessageOpen:
|
|
// Next token can be MessageClose, comma, semicolon or Name.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(MessageClose, 1, 0), nil
|
|
case otherCloseChar[closeCh]:
|
|
return Token{}, d.newSyntaxError(mismatchedFmt, ch)
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case ListOpen:
|
|
// Next token can be ListClose or comma
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(ListClose, 1, 0), nil
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
default:
|
|
return Token{}, d.newSyntaxError(unexpectedFmt, ch)
|
|
}
|
|
}
|
|
|
|
case ListOpen:
|
|
// Next token can be ListClose, MessageStart or Scalar.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case ']':
|
|
d.popOpenStack()
|
|
return d.consumeToken(ListClose, 1, 0), nil
|
|
case '{', '<':
|
|
d.pushOpenStack(ch)
|
|
return d.consumeToken(MessageOpen, 1, 0), nil
|
|
default:
|
|
return d.parseScalar()
|
|
}
|
|
|
|
case ListClose:
|
|
openKind, closeCh := d.currentOpenKind()
|
|
switch openKind {
|
|
case bof:
|
|
// Top level message.
|
|
// Next token can be EOF, comma, semicolon or Name.
|
|
if isEOF {
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case MessageOpen:
|
|
// Next token can be MessageClose, comma, semicolon or Name.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(MessageClose, 1, 0), nil
|
|
case otherCloseChar[closeCh]:
|
|
return Token{}, d.newSyntaxError(mismatchedFmt, ch)
|
|
case ',':
|
|
return d.consumeToken(comma, 1, 0), nil
|
|
case ';':
|
|
return d.consumeToken(semicolon, 1, 0), nil
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
default:
|
|
// It is not possible to have this case. Let it panic below.
|
|
}
|
|
|
|
case comma, semicolon:
|
|
openKind, closeCh := d.currentOpenKind()
|
|
switch openKind {
|
|
case bof:
|
|
// Top level message. Next token can be EOF or Name.
|
|
if isEOF {
|
|
return d.consumeToken(EOF, 0, 0), nil
|
|
}
|
|
return d.parseFieldName()
|
|
|
|
case MessageOpen:
|
|
// Next token can be MessageClose or Name.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case closeCh:
|
|
d.popOpenStack()
|
|
return d.consumeToken(MessageClose, 1, 0), nil
|
|
case otherCloseChar[closeCh]:
|
|
return Token{}, d.newSyntaxError(mismatchedFmt, ch)
|
|
default:
|
|
return d.parseFieldName()
|
|
}
|
|
|
|
case ListOpen:
|
|
if lastKind == semicolon {
|
|
// It is not be possible to have this case as logic here
|
|
// should not have produced a semicolon Token when inside a
|
|
// list. Let it panic below.
|
|
break
|
|
}
|
|
// Next token can be MessageOpen or Scalar.
|
|
if isEOF {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
switch ch := d.in[0]; ch {
|
|
case '{', '<':
|
|
d.pushOpenStack(ch)
|
|
return d.consumeToken(MessageOpen, 1, 0), nil
|
|
default:
|
|
return d.parseScalar()
|
|
}
|
|
}
|
|
}
|
|
|
|
line, column := d.Position(len(d.orig) - len(d.in))
|
|
panic(fmt.Sprintf("Decoder.parseNext: bug at handling line %d:%d with lastKind=%v", line, column, lastKind))
|
|
}
|
|
|
|
var otherCloseChar = map[byte]byte{
|
|
'}': '>',
|
|
'>': '}',
|
|
}
|
|
|
|
// currentOpenKind indicates whether current position is inside a message, list
|
|
// or top-level message by returning MessageOpen, ListOpen or bof respectively.
|
|
// If the returned kind is either a MessageOpen or ListOpen, it also returns the
|
|
// corresponding closing character.
|
|
func (d *Decoder) currentOpenKind() (Kind, byte) {
|
|
if len(d.openStack) == 0 {
|
|
return bof, 0
|
|
}
|
|
openCh := d.openStack[len(d.openStack)-1]
|
|
switch openCh {
|
|
case '{':
|
|
return MessageOpen, '}'
|
|
case '<':
|
|
return MessageOpen, '>'
|
|
case '[':
|
|
return ListOpen, ']'
|
|
}
|
|
panic(fmt.Sprintf("Decoder: openStack contains invalid byte %s", string(openCh)))
|
|
}
|
|
|
|
func (d *Decoder) pushOpenStack(ch byte) {
|
|
d.openStack = append(d.openStack, ch)
|
|
}
|
|
|
|
func (d *Decoder) popOpenStack() {
|
|
d.openStack = d.openStack[:len(d.openStack)-1]
|
|
}
|
|
|
|
// parseFieldName parses field name and separator.
|
|
func (d *Decoder) parseFieldName() (tok Token, err error) {
|
|
defer func() {
|
|
if err == nil && d.tryConsumeChar(':') {
|
|
tok.attrs |= hasSeparator
|
|
}
|
|
}()
|
|
|
|
// Extension or Any type URL.
|
|
if d.in[0] == '[' {
|
|
return d.parseTypeName()
|
|
}
|
|
|
|
// Identifier.
|
|
if size := parseIdent(d.in, false); size > 0 {
|
|
return d.consumeToken(Name, size, uint8(IdentName)), nil
|
|
}
|
|
|
|
// Field number. Identify if input is a valid number that is not negative
|
|
// and is decimal integer within 32-bit range.
|
|
if num := parseNumber(d.in); num.size > 0 {
|
|
if !num.neg && num.kind == numDec {
|
|
if _, err := strconv.ParseInt(string(d.in[:num.size]), 10, 32); err == nil {
|
|
return d.consumeToken(Name, num.size, uint8(FieldNumber)), nil
|
|
}
|
|
}
|
|
return Token{}, d.newSyntaxError("invalid field number: %s", d.in[:num.size])
|
|
}
|
|
|
|
return Token{}, d.newSyntaxError("invalid field name: %s", errRegexp.Find(d.in))
|
|
}
|
|
|
|
// parseTypeName parses Any type URL or extension field name. The name is
|
|
// enclosed in [ and ] characters. The C++ parser does not handle many legal URL
|
|
// strings. This implementation is more liberal and allows for the pattern
|
|
// ^[-_a-zA-Z0-9]+([./][-_a-zA-Z0-9]+)*`). Whitespaces and comments are allowed
|
|
// in between [ ], '.', '/' and the sub names.
|
|
func (d *Decoder) parseTypeName() (Token, error) {
|
|
startPos := len(d.orig) - len(d.in)
|
|
// Use alias s to advance first in order to use d.in for error handling.
|
|
// Caller already checks for [ as first character.
|
|
s := consume(d.in[1:], 0)
|
|
if len(s) == 0 {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
|
|
var name []byte
|
|
for len(s) > 0 && isTypeNameChar(s[0]) {
|
|
name = append(name, s[0])
|
|
s = s[1:]
|
|
}
|
|
s = consume(s, 0)
|
|
|
|
var closed bool
|
|
for len(s) > 0 && !closed {
|
|
switch {
|
|
case s[0] == ']':
|
|
s = s[1:]
|
|
closed = true
|
|
|
|
case s[0] == '/', s[0] == '.':
|
|
if len(name) > 0 && (name[len(name)-1] == '/' || name[len(name)-1] == '.') {
|
|
return Token{}, d.newSyntaxError("invalid type URL/extension field name: %s",
|
|
d.in[startPos:len(d.orig)-len(s)+1])
|
|
}
|
|
name = append(name, s[0])
|
|
s = s[1:]
|
|
s = consume(s, 0)
|
|
for len(s) > 0 && isTypeNameChar(s[0]) {
|
|
name = append(name, s[0])
|
|
s = s[1:]
|
|
}
|
|
s = consume(s, 0)
|
|
|
|
default:
|
|
return Token{}, d.newSyntaxError(
|
|
"invalid type URL/extension field name: %s", d.in[startPos:len(d.orig)-len(s)+1])
|
|
}
|
|
}
|
|
|
|
if !closed {
|
|
return Token{}, io.ErrUnexpectedEOF
|
|
}
|
|
|
|
// First character cannot be '.'. Last character cannot be '.' or '/'.
|
|
size := len(name)
|
|
if size == 0 || name[0] == '.' || name[size-1] == '.' || name[size-1] == '/' {
|
|
return Token{}, d.newSyntaxError("invalid type URL/extension field name: %s",
|
|
d.in[startPos:len(d.orig)-len(s)])
|
|
}
|
|
|
|
d.in = s
|
|
endPos := len(d.orig) - len(d.in)
|
|
d.consume(0)
|
|
|
|
return Token{
|
|
kind: Name,
|
|
attrs: uint8(TypeName),
|
|
pos: startPos,
|
|
raw: d.orig[startPos:endPos],
|
|
str: string(name),
|
|
}, nil
|
|
}
|
|
|
|
func isTypeNameChar(b byte) bool {
|
|
return (b == '-' || b == '_' ||
|
|
('0' <= b && b <= '9') ||
|
|
('a' <= b && b <= 'z') ||
|
|
('A' <= b && b <= 'Z'))
|
|
}
|
|
|
|
func isWhiteSpace(b byte) bool {
|
|
switch b {
|
|
case ' ', '\n', '\r', '\t':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// parseIdent parses an unquoted proto identifier and returns size.
|
|
// If allowNeg is true, it allows '-' to be the first character in the
|
|
// identifier. This is used when parsing literal values like -infinity, etc.
|
|
// Regular expression matches an identifier: `^[_a-zA-Z][_a-zA-Z0-9]*`
|
|
func parseIdent(input []byte, allowNeg bool) int {
|
|
var size int
|
|
|
|
s := input
|
|
if len(s) == 0 {
|
|
return 0
|
|
}
|
|
|
|
if allowNeg && s[0] == '-' {
|
|
s = s[1:]
|
|
size++
|
|
if len(s) == 0 {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case s[0] == '_',
|
|
'a' <= s[0] && s[0] <= 'z',
|
|
'A' <= s[0] && s[0] <= 'Z':
|
|
s = s[1:]
|
|
size++
|
|
default:
|
|
return 0
|
|
}
|
|
|
|
for len(s) > 0 && (s[0] == '_' ||
|
|
'a' <= s[0] && s[0] <= 'z' ||
|
|
'A' <= s[0] && s[0] <= 'Z' ||
|
|
'0' <= s[0] && s[0] <= '9') {
|
|
s = s[1:]
|
|
size++
|
|
}
|
|
|
|
if len(s) > 0 && !isDelim(s[0]) {
|
|
return 0
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
// parseScalar parses for a string, literal or number value.
|
|
func (d *Decoder) parseScalar() (Token, error) {
|
|
if d.in[0] == '"' || d.in[0] == '\'' {
|
|
return d.parseStringValue()
|
|
}
|
|
|
|
if tok, ok := d.parseLiteralValue(); ok {
|
|
return tok, nil
|
|
}
|
|
|
|
if tok, ok := d.parseNumberValue(); ok {
|
|
return tok, nil
|
|
}
|
|
|
|
return Token{}, d.newSyntaxError("invalid scalar value: %s", errRegexp.Find(d.in))
|
|
}
|
|
|
|
// parseLiteralValue parses a literal value. A literal value is used for
|
|
// bools, special floats and enums. This function simply identifies that the
|
|
// field value is a literal.
|
|
func (d *Decoder) parseLiteralValue() (Token, bool) {
|
|
size := parseIdent(d.in, true)
|
|
if size == 0 {
|
|
return Token{}, false
|
|
}
|
|
return d.consumeToken(Scalar, size, literalValue), true
|
|
}
|
|
|
|
// consumeToken constructs a Token for given Kind from d.in and consumes given
|
|
// size-length from it.
|
|
func (d *Decoder) consumeToken(kind Kind, size int, attrs uint8) Token {
|
|
// Important to compute raw and pos before consuming.
|
|
tok := Token{
|
|
kind: kind,
|
|
attrs: attrs,
|
|
pos: len(d.orig) - len(d.in),
|
|
raw: d.in[:size],
|
|
}
|
|
d.consume(size)
|
|
return tok
|
|
}
|
|
|
|
// newSyntaxError returns a syntax error with line and column information for
|
|
// current position.
|
|
func (d *Decoder) newSyntaxError(f string, x ...interface{}) error {
|
|
e := errors.New(f, x...)
|
|
line, column := d.Position(len(d.orig) - len(d.in))
|
|
return errors.New("syntax error (line %d:%d): %v", line, column, e)
|
|
}
|
|
|
|
// Position returns line and column number of given index of the original input.
|
|
// It will panic if index is out of range.
|
|
func (d *Decoder) Position(idx int) (line int, column int) {
|
|
b := d.orig[:idx]
|
|
line = bytes.Count(b, []byte("\n")) + 1
|
|
if i := bytes.LastIndexByte(b, '\n'); i >= 0 {
|
|
b = b[i+1:]
|
|
}
|
|
column = utf8.RuneCount(b) + 1 // ignore multi-rune characters
|
|
return line, column
|
|
}
|
|
|
|
func (d *Decoder) tryConsumeChar(c byte) bool {
|
|
if len(d.in) > 0 && d.in[0] == c {
|
|
d.consume(1)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// consume consumes n bytes of input and any subsequent whitespace or comments.
|
|
func (d *Decoder) consume(n int) {
|
|
d.in = consume(d.in, n)
|
|
return
|
|
}
|
|
|
|
// consume consumes n bytes of input and any subsequent whitespace or comments.
|
|
func consume(b []byte, n int) []byte {
|
|
b = b[n:]
|
|
for len(b) > 0 {
|
|
switch b[0] {
|
|
case ' ', '\n', '\r', '\t':
|
|
b = b[1:]
|
|
case '#':
|
|
if i := bytes.IndexByte(b, '\n'); i >= 0 {
|
|
b = b[i+len("\n"):]
|
|
} else {
|
|
b = nil
|
|
}
|
|
default:
|
|
return b
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Any sequence that looks like a non-delimiter (for error reporting).
|
|
var errRegexp = regexp.MustCompile(`^([-+._a-zA-Z0-9\/]+|.)`)
|
|
|
|
// isDelim returns true if given byte is a delimiter character.
|
|
func isDelim(c byte) bool {
|
|
return !(c == '-' || c == '+' || c == '.' || c == '_' ||
|
|
('a' <= c && c <= 'z') ||
|
|
('A' <= c && c <= 'Z') ||
|
|
('0' <= c && c <= '9'))
|
|
}
|