Edward McFarlane 05cbe34333 encoding: add MarshalAppend to protojson and prototext
Adds MarshalAppend methods to allow for byte slices to be reused.
Copies signature from the binary encoding.

Small changes to internal json and text libraries to use strconv
AppendInt and AppendUint for number encoding.

Change-Id: Ife7c8979c1c153a0a0bf9b70b296b8158d38dffc
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/489615
Reviewed-by: Edward McFarlane <emcfarlane000@gmail.com>
Reviewed-by: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Lasse Folger <lassefolger@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
2023-05-01 15:10:15 +00:00

558 lines
13 KiB
Go

// Copyright 2019 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_test
import (
"math"
"strings"
"testing"
"unicode/utf8"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/internal/detrand"
"google.golang.org/protobuf/internal/encoding/text"
)
// Disable detrand to enable direct comparisons on outputs.
func init() { detrand.Disable() }
func TestEncoder(t *testing.T) {
tests := []encoderTestCase{
{
desc: "no-opt",
write: func(e *text.Encoder) {},
wantOut: ``,
wantOutIndent: ``,
},
{
desc: "true",
write: func(e *text.Encoder) {
e.WriteName("bool")
e.WriteBool(true)
},
wantOut: `bool:true`,
wantOutIndent: `bool: true`,
},
{
desc: "false",
write: func(e *text.Encoder) {
e.WriteName("bool")
e.WriteBool(false)
},
wantOut: `bool:false`,
wantOutIndent: `bool: false`,
},
{
desc: "bracket name",
write: func(e *text.Encoder) {
e.WriteName("[extension]")
e.WriteString("hello")
},
wantOut: `[extension]:"hello"`,
wantOutIndent: `[extension]: "hello"`,
},
{
desc: "numeric name",
write: func(e *text.Encoder) {
e.WriteName("01234")
e.WriteString("hello")
},
wantOut: `01234:"hello"`,
wantOutIndent: `01234: "hello"`,
},
{
desc: "string",
write: func(e *text.Encoder) {
e.WriteName("str")
e.WriteString("hello world")
},
wantOut: `str:"hello world"`,
wantOutIndent: `str: "hello world"`,
},
{
desc: "enum",
write: func(e *text.Encoder) {
e.WriteName("enum")
e.WriteLiteral("ENUM_VALUE")
},
wantOut: `enum:ENUM_VALUE`,
wantOutIndent: `enum: ENUM_VALUE`,
},
{
desc: "float64",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(1.0199999809265137, 64)
},
wantOut: `float64:1.0199999809265137`,
wantOutIndent: `float64: 1.0199999809265137`,
},
{
desc: "float64 max value",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(math.MaxFloat64, 64)
},
wantOut: `float64:1.7976931348623157e+308`,
wantOutIndent: `float64: 1.7976931348623157e+308`,
},
{
desc: "float64 min value",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(-math.MaxFloat64, 64)
},
wantOut: `float64:-1.7976931348623157e+308`,
wantOutIndent: `float64: -1.7976931348623157e+308`,
},
{
desc: "float64 nan",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(math.NaN(), 64)
},
wantOut: `float64:nan`,
wantOutIndent: `float64: nan`,
},
{
desc: "float64 inf",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(math.Inf(+1), 64)
},
wantOut: `float64:inf`,
wantOutIndent: `float64: inf`,
},
{
desc: "float64 -inf",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(math.Inf(-1), 64)
},
wantOut: `float64:-inf`,
wantOutIndent: `float64: -inf`,
},
{
desc: "float64 negative zero",
write: func(e *text.Encoder) {
e.WriteName("float64")
e.WriteFloat(math.Copysign(0, -1), 64)
},
wantOut: `float64:-0`,
wantOutIndent: `float64: -0`,
},
{
desc: "float32",
write: func(e *text.Encoder) {
e.WriteName("float")
e.WriteFloat(1.02, 32)
},
wantOut: `float:1.02`,
wantOutIndent: `float: 1.02`,
},
{
desc: "float32 max value",
write: func(e *text.Encoder) {
e.WriteName("float32")
e.WriteFloat(math.MaxFloat32, 32)
},
wantOut: `float32:3.4028235e+38`,
wantOutIndent: `float32: 3.4028235e+38`,
},
{
desc: "float32 nan",
write: func(e *text.Encoder) {
e.WriteName("float32")
e.WriteFloat(math.NaN(), 32)
},
wantOut: `float32:nan`,
wantOutIndent: `float32: nan`,
},
{
desc: "float32 inf",
write: func(e *text.Encoder) {
e.WriteName("float32")
e.WriteFloat(math.Inf(+1), 32)
},
wantOut: `float32:inf`,
wantOutIndent: `float32: inf`,
},
{
desc: "float32 -inf",
write: func(e *text.Encoder) {
e.WriteName("float32")
e.WriteFloat(math.Inf(-1), 32)
},
wantOut: `float32:-inf`,
wantOutIndent: `float32: -inf`,
},
{
desc: "float32 negative zero",
write: func(e *text.Encoder) {
e.WriteName("float32")
e.WriteFloat(math.Copysign(0, -1), 32)
},
wantOut: `float32:-0`,
wantOutIndent: `float32: -0`,
},
{
desc: "int64 max value",
write: func(e *text.Encoder) {
e.WriteName("int")
e.WriteInt(math.MaxInt64)
},
wantOut: `int:9223372036854775807`,
wantOutIndent: `int: 9223372036854775807`,
},
{
desc: "int64 min value",
write: func(e *text.Encoder) {
e.WriteName("int")
e.WriteInt(math.MinInt64)
},
wantOut: `int:-9223372036854775808`,
wantOutIndent: `int: -9223372036854775808`,
},
{
desc: "uint",
write: func(e *text.Encoder) {
e.WriteName("uint")
e.WriteUint(math.MaxUint64)
},
wantOut: `uint:18446744073709551615`,
wantOutIndent: `uint: 18446744073709551615`,
},
{
desc: "empty message field",
write: func(e *text.Encoder) {
e.WriteName("m")
e.StartMessage()
e.EndMessage()
},
wantOut: `m:{}`,
wantOutIndent: `m: {}`,
},
{
desc: "multiple fields",
write: func(e *text.Encoder) {
e.WriteName("bool")
e.WriteBool(true)
e.WriteName("str")
e.WriteString("hello")
e.WriteName("str")
e.WriteString("world")
e.WriteName("m")
e.StartMessage()
e.EndMessage()
e.WriteName("[int]")
e.WriteInt(49)
e.WriteName("float64")
e.WriteFloat(1.00023e4, 64)
e.WriteName("101")
e.WriteString("unknown")
},
wantOut: `bool:true str:"hello" str:"world" m:{} [int]:49 float64:10002.3 101:"unknown"`,
wantOutIndent: `bool: true
str: "hello"
str: "world"
m: {}
[int]: 49
float64: 10002.3
101: "unknown"`,
},
{
desc: "populated message fields",
write: func(e *text.Encoder) {
e.WriteName("m1")
e.StartMessage()
{
e.WriteName("str")
e.WriteString("hello")
}
e.EndMessage()
e.WriteName("bool")
e.WriteBool(true)
e.WriteName("m2")
e.StartMessage()
{
e.WriteName("str")
e.WriteString("world")
e.WriteName("m2-1")
e.StartMessage()
e.EndMessage()
e.WriteName("m2-2")
e.StartMessage()
{
e.WriteName("[int]")
e.WriteInt(49)
}
e.EndMessage()
e.WriteName("float64")
e.WriteFloat(1.00023e4, 64)
}
e.EndMessage()
e.WriteName("101")
e.WriteString("unknown")
},
wantOut: `m1:{str:"hello"} bool:true m2:{str:"world" m2-1:{} m2-2:{[int]:49} float64:10002.3} 101:"unknown"`,
wantOutIndent: `m1: {
str: "hello"
}
bool: true
m2: {
str: "world"
m2-1: {}
m2-2: {
[int]: 49
}
float64: 10002.3
}
101: "unknown"`,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
runEncoderTest(t, tc, [2]byte{})
// Test using the angle brackets.
// Testcases should not contain characters '{' and '}'.
tc.wantOut = replaceDelims(tc.wantOut)
tc.wantOutIndent = replaceDelims(tc.wantOutIndent)
runEncoderTest(t, tc, [2]byte{'<', '>'})
})
}
}
type encoderTestCase struct {
desc string
write func(*text.Encoder)
wantOut string
wantOutIndent string
}
func runEncoderTest(t *testing.T, tc encoderTestCase, delims [2]byte) {
t.Helper()
if tc.wantOut != "" {
enc, err := text.NewEncoder(nil, "", delims, false)
if err != nil {
t.Fatalf("NewEncoder returned error: %v", err)
}
tc.write(enc)
got := string(enc.Bytes())
if got != tc.wantOut {
t.Errorf("(compact)\n<got>\n%v\n<want>\n%v\n", got, tc.wantOut)
}
}
if tc.wantOutIndent != "" {
enc, err := text.NewEncoder(nil, "\t", delims, false)
if err != nil {
t.Fatalf("NewEncoder returned error: %v", err)
}
tc.write(enc)
got, want := string(enc.Bytes()), tc.wantOutIndent
if got != want {
t.Errorf("(multi-line)\n<got>\n%v\n<want>\n%v\n<diff -want +got>\n%v\n",
got, want, cmp.Diff(want, got))
}
}
}
func replaceDelims(s string) string {
s = strings.Replace(s, "{", "<", -1)
return strings.Replace(s, "}", ">", -1)
}
// Test for UTF-8 and ASCII outputs.
func TestEncodeStrings(t *testing.T) {
tests := []struct {
in string
wantOut string
wantOutASCII string
}{
{
in: `"`,
wantOut: `"\""`,
},
{
in: `'`,
wantOut: `"'"`,
},
{
in: "hello\u1234world",
wantOut: "\"hello\u1234world\"",
wantOutASCII: `"hello\u1234world"`,
},
{
// String that has as few escaped characters as possible.
in: func() string {
var b []byte
for i := rune(0); i <= 0x00a0; i++ {
switch i {
case 0, '\\', '\n', '\'': // these must be escaped, so ignore them
default:
var r [utf8.UTFMax]byte
n := utf8.EncodeRune(r[:], i)
b = append(b, r[:n]...)
}
}
return string(b)
}(),
wantOut: `"\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_` + "`" + `abcdefghijklmnopqrstuvwxyz{|}~\x7f\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f` + "\u00a0" + `"`,
wantOutASCII: `"\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_` + "`" + `abcdefghijklmnopqrstuvwxyz{|}~\x7f\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f\u00a0"`,
},
{
// Valid UTF-8 wire encoding of the RuneError rune.
in: string(utf8.RuneError),
wantOut: `"` + string(utf8.RuneError) + `"`,
wantOutASCII: `"\ufffd"`,
},
{
in: "\"'\\?\a\b\n\r\t\v\f\x01\nS\n\xab\x12\uab8f\U0010ffff",
wantOut: `"\"'\\?\x07\x08\n\r\t\x0b\x0c\x01\nS\n\xab\x12` + "\uab8f\U0010ffff" + `"`,
wantOutASCII: `"\"'\\?\x07\x08\n\r\t\x0b\x0c\x01\nS\n\xab\x12\uab8f\U0010ffff"`,
},
{
in: "\001x",
wantOut: `"\x01x"`,
wantOutASCII: `"\x01x"`,
},
{
in: "\012x",
wantOut: `"\nx"`,
wantOutASCII: `"\nx"`,
},
{
in: "\123x",
wantOut: `"Sx"`,
wantOutASCII: `"Sx"`,
},
{
in: "\1234x",
wantOut: `"S4x"`,
wantOutASCII: `"S4x"`,
},
{
in: "\001",
wantOut: `"\x01"`,
wantOutASCII: `"\x01"`,
},
{
in: "\012",
wantOut: `"\n"`,
wantOutASCII: `"\n"`,
},
{
in: "\123",
wantOut: `"S"`,
wantOutASCII: `"S"`,
},
{
in: "\1234",
wantOut: `"S4"`,
wantOutASCII: `"S4"`,
},
{
in: "\377",
wantOut: `"\xff"`,
wantOutASCII: `"\xff"`,
},
{
in: "\x0fx",
wantOut: `"\x0fx"`,
wantOutASCII: `"\x0fx"`,
},
{
in: "\xffx",
wantOut: `"\xffx"`,
wantOutASCII: `"\xffx"`,
},
{
in: "\xfffx",
wantOut: `"\xfffx"`,
wantOutASCII: `"\xfffx"`,
},
{
in: "\x0f",
wantOut: `"\x0f"`,
wantOutASCII: `"\x0f"`,
},
{
in: "\x7f",
wantOut: `"\x7f"`,
wantOutASCII: `"\x7f"`,
},
{
in: "\xff",
wantOut: `"\xff"`,
wantOutASCII: `"\xff"`,
},
{
in: "\xfff",
wantOut: `"\xfff"`,
wantOutASCII: `"\xfff"`,
},
}
for _, tc := range tests {
t.Run("", func(t *testing.T) {
if tc.wantOut != "" {
runEncodeStringsTest(t, tc.in, tc.wantOut, false)
}
if tc.wantOutASCII != "" {
runEncodeStringsTest(t, tc.in, tc.wantOutASCII, true)
}
})
}
}
func runEncodeStringsTest(t *testing.T, in string, want string, outputASCII bool) {
t.Helper()
charType := "UTF-8"
if outputASCII {
charType = "ASCII"
}
enc, err := text.NewEncoder(nil, "", [2]byte{}, outputASCII)
if err != nil {
t.Fatalf("[%s] NewEncoder returned error: %v", charType, err)
}
enc.WriteString(in)
got := string(enc.Bytes())
if got != want {
t.Errorf("[%s] WriteString(%q)\n<got>\n%v\n<want>\n%v\n", charType, in, got, want)
}
}
func TestReset(t *testing.T) {
enc, err := text.NewEncoder(nil, "\t", [2]byte{}, false)
if err != nil {
t.Fatalf("NewEncoder returned error: %v", err)
}
enc.WriteName("foo")
pos := enc.Snapshot()
// Attempt to write a message value.
enc.StartMessage()
enc.WriteName("bar")
enc.WriteUint(10)
// Reset the value and decided to write a string value instead.
enc.Reset(pos)
enc.WriteString("0123456789")
got := string(enc.Bytes())
want := `foo: "0123456789"`
if got != want {
t.Errorf("Reset did not restore given position:\n<got>\n%v\n<want>\n%v\n", got, want)
}
}