encoding/textpb: add marshaling of unknown fields

Change-Id: Ifa2a86b3edd51d6c53d9cf7226b6f4f8d2f88a44
Reviewed-on: https://go-review.googlesource.com/c/153757
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/textpb/encode.go b/encoding/textpb/encode.go
index 5d00cc2..8d4b9cb 100644
--- a/encoding/textpb/encode.go
+++ b/encoding/textpb/encode.go
@@ -5,9 +5,11 @@
 package textpb
 
 import (
+	"fmt"
 	"sort"
 
 	"github.com/golang/protobuf/v2/internal/encoding/text"
+	"github.com/golang/protobuf/v2/internal/encoding/wire"
 	"github.com/golang/protobuf/v2/internal/errors"
 	"github.com/golang/protobuf/v2/internal/pragma"
 	"github.com/golang/protobuf/v2/proto"
@@ -106,10 +108,16 @@
 			}
 			msgFields = append(msgFields, [2]text.Value{tname, tval})
 		}
-
 	}
 
-	// TODO: Handle extensions, unknowns and Any.
+	// Marshal out unknown fields.
+	// TODO: Provide option to exclude or include unknown fields.
+	m.UnknownFields().Range(func(_ pref.FieldNumber, raw pref.RawFields) bool {
+		msgFields = appendUnknown(msgFields, raw)
+		return true
+	})
+
+	// TODO: Handle extensions and Any expansion.
 
 	return text.ValueOf(msgFields), nerr.E
 }
@@ -243,3 +251,35 @@
 	}
 	sort.Slice(values, less)
 }
+
+// appendUnknown parses the given []byte and appends field(s) into the given fields slice.
+// This function assumes proper encoding in the given []byte.
+func appendUnknown(fields [][2]text.Value, b []byte) [][2]text.Value {
+	for len(b) > 0 {
+		var value interface{}
+		num, wtype, n := wire.ConsumeTag(b)
+		b = b[n:]
+
+		switch wtype {
+		case wire.VarintType:
+			value, n = wire.ConsumeVarint(b)
+		case wire.Fixed32Type:
+			value, n = wire.ConsumeFixed32(b)
+		case wire.Fixed64Type:
+			value, n = wire.ConsumeFixed64(b)
+		case wire.BytesType:
+			value, n = wire.ConsumeBytes(b)
+		case wire.StartGroupType:
+			var v []byte
+			v, n = wire.ConsumeGroup(num, b)
+			var msg [][2]text.Value
+			value = appendUnknown(msg, v)
+		default:
+			panic(fmt.Sprintf("error parsing unknown field wire type: %v", wtype))
+		}
+
+		fields = append(fields, [2]text.Value{text.ValueOf(uint32(num)), text.ValueOf(value)})
+		b = b[n:]
+	}
+	return fields
+}
diff --git a/encoding/textpb/encode_test.go b/encoding/textpb/encode_test.go
index 498b2a7..7c9a912 100644
--- a/encoding/textpb/encode_test.go
+++ b/encoding/textpb/encode_test.go
@@ -12,6 +12,7 @@
 	protoV1 "github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/v2/encoding/textpb"
 	"github.com/golang/protobuf/v2/internal/detrand"
+	"github.com/golang/protobuf/v2/internal/encoding/pack"
 	"github.com/golang/protobuf/v2/internal/impl"
 	"github.com/golang/protobuf/v2/internal/scalar"
 	"github.com/golang/protobuf/v2/proto"
@@ -737,6 +738,68 @@
 }
 `,
 		wantErr: true,
+	}, {
+		desc: "unknown varint and fixed types",
+		input: &pb2.Scalars{
+			OptString: scalar.String("this message contains unknown fields"),
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.VarintType}, pack.Bool(true),
+				pack.Tag{102, pack.VarintType}, pack.Varint(0xff),
+				pack.Tag{103, pack.Fixed32Type}, pack.Uint32(47),
+				pack.Tag{104, pack.Fixed64Type}, pack.Int64(0xdeadbeef),
+			}.Marshal(),
+		},
+		want: `opt_string: "this message contains unknown fields"
+101: 1
+102: 255
+103: 47
+104: 3735928559
+`,
+	}, {
+		desc: "unknown length-delimited",
+		input: &pb2.Scalars{
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.BytesType}, pack.LengthPrefix{pack.Bool(true), pack.Bool(false)},
+				pack.Tag{102, pack.BytesType}, pack.String("hello world"),
+				pack.Tag{103, pack.BytesType}, pack.Bytes("\xe4\xb8\x96\xe7\x95\x8c"),
+			}.Marshal(),
+		},
+		want: `101: "\x01\x00"
+102: "hello world"
+103: "世界"
+`,
+	}, {
+		desc: "unknown group type",
+		input: &pb2.Scalars{
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.StartGroupType}, pack.Tag{101, pack.EndGroupType},
+				pack.Tag{102, pack.StartGroupType},
+				pack.Tag{101, pack.VarintType}, pack.Bool(false),
+				pack.Tag{102, pack.BytesType}, pack.String("inside a group"),
+				pack.Tag{102, pack.EndGroupType},
+			}.Marshal(),
+		},
+		want: `101: {}
+102: {
+  101: 0
+  102: "inside a group"
+}
+`,
+	}, {
+		desc: "unknown unpack repeated field",
+		input: &pb2.Scalars{
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.BytesType}, pack.LengthPrefix{pack.Bool(true), pack.Bool(false), pack.Bool(true)},
+				pack.Tag{102, pack.BytesType}, pack.String("hello"),
+				pack.Tag{101, pack.VarintType}, pack.Bool(true),
+				pack.Tag{102, pack.BytesType}, pack.String("世界"),
+			}.Marshal(),
+		},
+		want: `101: "\x01\x00\x01"
+101: 1
+102: "hello"
+102: "世界"
+`,
 	}}
 
 	for _, tt := range tests {