mirror of
https://github.com/protocolbuffers/protobuf-go.git
synced 2025-01-10 07:05:11 +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>
371 lines
9.2 KiB
Go
371 lines
9.2 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"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/internal/flags"
|
|
)
|
|
|
|
// Kind represents a token kind expressible in the textproto format.
|
|
type Kind uint8
|
|
|
|
const (
|
|
Invalid Kind = iota
|
|
EOF
|
|
Name // Name indicates the field name.
|
|
Scalar // Scalar are scalar values, e.g. "string", 47, ENUM_LITERAL, true.
|
|
MessageOpen
|
|
MessageClose
|
|
ListOpen
|
|
ListClose
|
|
|
|
// comma and semi-colon are only for parsing in between values and should not be exposed.
|
|
comma
|
|
semicolon
|
|
|
|
// bof indicates beginning of file, which is the default token
|
|
// kind at the beginning of parsing.
|
|
bof = Invalid
|
|
)
|
|
|
|
func (t Kind) String() string {
|
|
switch t {
|
|
case Invalid:
|
|
return "<invalid>"
|
|
case EOF:
|
|
return "eof"
|
|
case Scalar:
|
|
return "scalar"
|
|
case Name:
|
|
return "name"
|
|
case MessageOpen:
|
|
return "{"
|
|
case MessageClose:
|
|
return "}"
|
|
case ListOpen:
|
|
return "["
|
|
case ListClose:
|
|
return "]"
|
|
case comma:
|
|
return ","
|
|
case semicolon:
|
|
return ";"
|
|
default:
|
|
return fmt.Sprintf("<invalid:%v>", uint8(t))
|
|
}
|
|
}
|
|
|
|
// NameKind represents different types of field names.
|
|
type NameKind uint8
|
|
|
|
const (
|
|
IdentName NameKind = iota + 1
|
|
TypeName
|
|
FieldNumber
|
|
)
|
|
|
|
func (t NameKind) String() string {
|
|
switch t {
|
|
case IdentName:
|
|
return "IdentName"
|
|
case TypeName:
|
|
return "TypeName"
|
|
case FieldNumber:
|
|
return "FieldNumber"
|
|
default:
|
|
return fmt.Sprintf("<invalid:%v>", uint8(t))
|
|
}
|
|
}
|
|
|
|
// Bit mask in Token.attrs to indicate if a Name token is followed by the
|
|
// separator char ':'. The field name separator char is optional for message
|
|
// field or repeated message field, but required for all other types. Decoder
|
|
// simply indicates whether a Name token is followed by separator or not. It is
|
|
// up to the prototext package to validate.
|
|
const hasSeparator = 1 << 7
|
|
|
|
// Scalar value types.
|
|
const (
|
|
numberValue = iota + 1
|
|
stringValue
|
|
literalValue
|
|
)
|
|
|
|
// Bit mask in Token.numAttrs to indicate that the number is a negative.
|
|
const isNegative = 1 << 7
|
|
|
|
// Token provides a parsed token kind and value. Values are provided by the
|
|
// different accessor methods.
|
|
type Token struct {
|
|
// Kind of the Token object.
|
|
kind Kind
|
|
// attrs contains metadata for the following Kinds:
|
|
// Name: hasSeparator bit and one of NameKind.
|
|
// Scalar: one of numberValue, stringValue, literalValue.
|
|
attrs uint8
|
|
// numAttrs contains metadata for numberValue:
|
|
// - highest bit is whether negative or positive.
|
|
// - lower bits indicate one of numDec, numHex, numOct, numFloat.
|
|
numAttrs uint8
|
|
// pos provides the position of the token in the original input.
|
|
pos int
|
|
// raw bytes of the serialized token.
|
|
// This is a subslice into the original input.
|
|
raw []byte
|
|
// str contains parsed string for the following:
|
|
// - stringValue of Scalar kind
|
|
// - numberValue of Scalar kind
|
|
// - TypeName of Name kind
|
|
str string
|
|
}
|
|
|
|
// Kind returns the token kind.
|
|
func (t Token) Kind() Kind {
|
|
return t.kind
|
|
}
|
|
|
|
// RawString returns the read value in string.
|
|
func (t Token) RawString() string {
|
|
return string(t.raw)
|
|
}
|
|
|
|
// Pos returns the token position from the input.
|
|
func (t Token) Pos() int {
|
|
return t.pos
|
|
}
|
|
|
|
// NameKind returns IdentName, TypeName or FieldNumber.
|
|
// It panics if type is not Name.
|
|
func (t Token) NameKind() NameKind {
|
|
if t.kind == Name {
|
|
return NameKind(t.attrs &^ hasSeparator)
|
|
}
|
|
panic(fmt.Sprintf("Token is not a Name type: %s", t.kind))
|
|
}
|
|
|
|
// HasSeparator returns true if the field name is followed by the separator char
|
|
// ':', else false. It panics if type is not Name.
|
|
func (t Token) HasSeparator() bool {
|
|
if t.kind == Name {
|
|
return t.attrs&hasSeparator != 0
|
|
}
|
|
panic(fmt.Sprintf("Token is not a Name type: %s", t.kind))
|
|
}
|
|
|
|
// IdentName returns the value for IdentName type.
|
|
func (t Token) IdentName() string {
|
|
if t.kind == Name && t.attrs&uint8(IdentName) != 0 {
|
|
return string(t.raw)
|
|
}
|
|
panic(fmt.Sprintf("Token is not an IdentName: %s:%s", t.kind, NameKind(t.attrs&^hasSeparator)))
|
|
}
|
|
|
|
// TypeName returns the value for TypeName type.
|
|
func (t Token) TypeName() string {
|
|
if t.kind == Name && t.attrs&uint8(TypeName) != 0 {
|
|
return t.str
|
|
}
|
|
panic(fmt.Sprintf("Token is not a TypeName: %s:%s", t.kind, NameKind(t.attrs&^hasSeparator)))
|
|
}
|
|
|
|
// FieldNumber returns the value for FieldNumber type. It returns a
|
|
// non-negative int32 value. Caller will still need to validate for the correct
|
|
// field number range.
|
|
func (t Token) FieldNumber() int32 {
|
|
if t.kind != Name || t.attrs&uint8(FieldNumber) == 0 {
|
|
panic(fmt.Sprintf("Token is not a FieldNumber: %s:%s", t.kind, NameKind(t.attrs&^hasSeparator)))
|
|
}
|
|
// Following should not return an error as it had already been called right
|
|
// before this Token was constructed.
|
|
num, _ := strconv.ParseInt(string(t.raw), 10, 32)
|
|
return int32(num)
|
|
}
|
|
|
|
// String returns the string value for a Scalar type.
|
|
func (t Token) String() (string, bool) {
|
|
if t.kind != Scalar || t.attrs != stringValue {
|
|
return "", false
|
|
}
|
|
return t.str, true
|
|
}
|
|
|
|
// Enum returns the literal value for a Scalar type for use as enum literals.
|
|
func (t Token) Enum() (string, bool) {
|
|
if t.kind != Scalar || t.attrs != literalValue || (len(t.raw) > 0 && t.raw[0] == '-') {
|
|
return "", false
|
|
}
|
|
return string(t.raw), true
|
|
}
|
|
|
|
// Bool returns the bool value for a Scalar type.
|
|
func (t Token) Bool() (bool, bool) {
|
|
if t.kind != Scalar {
|
|
return false, false
|
|
}
|
|
switch t.attrs {
|
|
case literalValue:
|
|
if b, ok := boolLits[string(t.raw)]; ok {
|
|
return b, true
|
|
}
|
|
case numberValue:
|
|
// Unsigned integer representation of 0 or 1 is permitted: 00, 0x0, 01,
|
|
// 0x1, etc.
|
|
n, err := strconv.ParseUint(t.str, 0, 64)
|
|
if err == nil {
|
|
switch n {
|
|
case 0:
|
|
return false, true
|
|
case 1:
|
|
return true, true
|
|
}
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// These exact boolean literals are the ones supported in C++.
|
|
var boolLits = map[string]bool{
|
|
"t": true,
|
|
"true": true,
|
|
"True": true,
|
|
"f": false,
|
|
"false": false,
|
|
"False": false,
|
|
}
|
|
|
|
// Uint64 returns the uint64 value for a Scalar type.
|
|
func (t Token) Uint64() (uint64, bool) {
|
|
if t.kind != Scalar || t.attrs != numberValue ||
|
|
t.numAttrs&isNegative > 0 || t.numAttrs&numFloat > 0 {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(t.str, 0, 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
// Uint32 returns the uint32 value for a Scalar type.
|
|
func (t Token) Uint32() (uint32, bool) {
|
|
if t.kind != Scalar || t.attrs != numberValue ||
|
|
t.numAttrs&isNegative > 0 || t.numAttrs&numFloat > 0 {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(t.str, 0, 32)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return uint32(n), true
|
|
}
|
|
|
|
// Int64 returns the int64 value for a Scalar type.
|
|
func (t Token) Int64() (int64, bool) {
|
|
if t.kind != Scalar || t.attrs != numberValue || t.numAttrs&numFloat > 0 {
|
|
return 0, false
|
|
}
|
|
if n, err := strconv.ParseInt(t.str, 0, 64); err == nil {
|
|
return n, true
|
|
}
|
|
// C++ accepts large positive hex numbers as negative values.
|
|
// This feature is here for proto1 backwards compatibility purposes.
|
|
if flags.ProtoLegacy && (t.numAttrs == numHex) {
|
|
if n, err := strconv.ParseUint(t.str, 0, 64); err == nil {
|
|
return int64(n), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Int32 returns the int32 value for a Scalar type.
|
|
func (t Token) Int32() (int32, bool) {
|
|
if t.kind != Scalar || t.attrs != numberValue || t.numAttrs&numFloat > 0 {
|
|
return 0, false
|
|
}
|
|
if n, err := strconv.ParseInt(t.str, 0, 32); err == nil {
|
|
return int32(n), true
|
|
}
|
|
// C++ accepts large positive hex numbers as negative values.
|
|
// This feature is here for proto1 backwards compatibility purposes.
|
|
if flags.ProtoLegacy && (t.numAttrs == numHex) {
|
|
if n, err := strconv.ParseUint(t.str, 0, 32); err == nil {
|
|
return int32(n), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Float64 returns the float64 value for a Scalar type.
|
|
func (t Token) Float64() (float64, bool) {
|
|
if t.kind != Scalar {
|
|
return 0, false
|
|
}
|
|
switch t.attrs {
|
|
case literalValue:
|
|
if f, ok := floatLits[strings.ToLower(string(t.raw))]; ok {
|
|
return f, true
|
|
}
|
|
case numberValue:
|
|
n, err := strconv.ParseFloat(t.str, 64)
|
|
if err == nil {
|
|
return n, true
|
|
}
|
|
nerr := err.(*strconv.NumError)
|
|
if nerr.Err == strconv.ErrRange {
|
|
return n, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Float32 returns the float32 value for a Scalar type.
|
|
func (t Token) Float32() (float32, bool) {
|
|
if t.kind != Scalar {
|
|
return 0, false
|
|
}
|
|
switch t.attrs {
|
|
case literalValue:
|
|
if f, ok := floatLits[strings.ToLower(string(t.raw))]; ok {
|
|
return float32(f), true
|
|
}
|
|
case numberValue:
|
|
n, err := strconv.ParseFloat(t.str, 64)
|
|
if err == nil {
|
|
// Overflows are treated as (-)infinity.
|
|
return float32(n), true
|
|
}
|
|
nerr := err.(*strconv.NumError)
|
|
if nerr.Err == strconv.ErrRange {
|
|
return float32(n), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// These are the supported float literals which C++ permits case-insensitive
|
|
// variants of these.
|
|
var floatLits = map[string]float64{
|
|
"nan": math.NaN(),
|
|
"inf": math.Inf(1),
|
|
"infinity": math.Inf(1),
|
|
"-inf": math.Inf(-1),
|
|
"-infinity": math.Inf(-1),
|
|
}
|
|
|
|
// TokenEquals returns true if given Tokens are equal, else false.
|
|
func TokenEquals(x, y Token) bool {
|
|
return x.kind == y.kind &&
|
|
x.attrs == y.attrs &&
|
|
x.numAttrs == y.numAttrs &&
|
|
x.pos == y.pos &&
|
|
bytes.Equal(x.raw, y.raw) &&
|
|
x.str == y.str
|
|
}
|