internal/encoding/tag: centralize logic for protobuf struct tag serialization

The bespoke text-serialization of field descriptors in protoc-gen-go is also
used in the legacy implementation of protobuf reflection to derive a
protoreflect.FieldDescriptor from legacy messages and also to convert to/from
protoreflect.ExtensionDescriptor and protoV1.ExtensionDesc.

Centralize this logic in a single place:
* to avoid reimplementing the same logic in internal/impl
* to keep the marshal and unmarshal logic co-located

Change-Id: I634c5afbb9dc6eda91d6cb6b0e68dbd724cb1ccb
Reviewed-on: https://go-review.googlesource.com/c/146758
Reviewed-by: Herbie Ong <herbie@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/cmd/protoc-gen-go/internal_gengo/main.go b/cmd/protoc-gen-go/internal_gengo/main.go
index b5b6655..8bc1b81 100644
--- a/cmd/protoc-gen-go/internal_gengo/main.go
+++ b/cmd/protoc-gen-go/internal_gengo/main.go
@@ -18,6 +18,7 @@
 
 	"github.com/golang/protobuf/proto"
 	descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/golang/protobuf/v2/internal/encoding/tag"
 	"github.com/golang/protobuf/v2/protogen"
 	"github.com/golang/protobuf/v2/reflect/protoreflect"
 )
@@ -636,112 +637,11 @@
 }
 
 func fieldProtobufTag(field *protogen.Field) string {
-	var tag []string
-	// wire type
-	tag = append(tag, wireTypes[field.Desc.Kind()])
-	// field number
-	tag = append(tag, strconv.Itoa(int(field.Desc.Number())))
-	// cardinality
-	switch field.Desc.Cardinality() {
-	case protoreflect.Optional:
-		tag = append(tag, "opt")
-	case protoreflect.Required:
-		tag = append(tag, "req")
-	case protoreflect.Repeated:
-		tag = append(tag, "rep")
-	}
-	if field.Desc.IsPacked() {
-		tag = append(tag, "packed")
-	}
-	// TODO: packed
-	// name
-	name := string(field.Desc.Name())
-	if field.Desc.Kind() == protoreflect.GroupKind {
-		// The name of the FieldDescriptor for a group field is
-		// lowercased. To find the original capitalization, we
-		// look in the field's MessageType.
-		name = string(field.MessageType.Desc.Name())
-	}
-	tag = append(tag, "name="+name)
-	// JSON name
-	if jsonName := field.Desc.JSONName(); jsonName != "" && jsonName != name {
-		tag = append(tag, "json="+jsonName)
-	}
-	// proto3
-	// The previous implementation does not tag extension fields as proto3,
-	// even when the field is defined in a proto3 file. Match that behavior
-	// for consistency.
-	if field.Desc.Syntax() == protoreflect.Proto3 && field.Desc.ExtendedType() == nil {
-		tag = append(tag, "proto3")
-	}
-	// enum
+	var enumName string
 	if field.Desc.Kind() == protoreflect.EnumKind {
-		tag = append(tag, "enum="+enumRegistryName(field.EnumType))
+		enumName = enumRegistryName(field.EnumType)
 	}
-	// oneof
-	if field.Desc.OneofType() != nil {
-		tag = append(tag, "oneof")
-	}
-	// default value
-	// This must appear last in the tag, since commas in strings aren't escaped.
-	if field.Desc.HasDefault() {
-		var def string
-		switch field.Desc.Kind() {
-		case protoreflect.BoolKind:
-			if field.Desc.Default().Bool() {
-				def = "1"
-			} else {
-				def = "0"
-			}
-		case protoreflect.BytesKind:
-			// Preserve protoc-gen-go's historical output of escaped bytes.
-			// This behavior is buggy, but fixing it makes it impossible to
-			// distinguish between the escaped and unescaped forms.
-			//
-			// To match the exact output of protoc, this is identical to the
-			// CEscape function in strutil.cc of the protoc source code.
-			var b []byte
-			for _, c := range field.Desc.Default().Bytes() {
-				switch c {
-				case '\n':
-					b = append(b, `\n`...)
-				case '\r':
-					b = append(b, `\r`...)
-				case '\t':
-					b = append(b, `\t`...)
-				case '"':
-					b = append(b, `\"`...)
-				case '\'':
-					b = append(b, `\'`...)
-				case '\\':
-					b = append(b, `\\`...)
-				default:
-					if c >= 0x20 && c <= 0x7e {
-						b = append(b, c)
-					} else {
-						b = append(b, fmt.Sprintf(`\%03o`, c)...)
-					}
-				}
-			}
-			def = string(b)
-		case protoreflect.FloatKind, protoreflect.DoubleKind:
-			f := field.Desc.Default().Float()
-			switch {
-			case math.IsInf(f, -1):
-				def = "-inf"
-			case math.IsInf(f, 1):
-				def = "inf"
-			case math.IsNaN(f):
-				def = "nan"
-			default:
-				def = fmt.Sprint(field.Desc.Default().Interface())
-			}
-		default:
-			def = fmt.Sprint(field.Desc.Default().Interface())
-		}
-		tag = append(tag, "def="+def)
-	}
-	return strings.Join(tag, ",")
+	return tag.Marshal(field.Desc, enumName)
 }
 
 func fieldDefaultValue(g *protogen.GeneratedFile, message *protogen.Message, field *protogen.Field) string {
@@ -789,27 +689,6 @@
 	return true
 }
 
-var wireTypes = map[protoreflect.Kind]string{
-	protoreflect.BoolKind:     "varint",
-	protoreflect.EnumKind:     "varint",
-	protoreflect.Int32Kind:    "varint",
-	protoreflect.Sint32Kind:   "zigzag32",
-	protoreflect.Uint32Kind:   "varint",
-	protoreflect.Int64Kind:    "varint",
-	protoreflect.Sint64Kind:   "zigzag64",
-	protoreflect.Uint64Kind:   "varint",
-	protoreflect.Sfixed32Kind: "fixed32",
-	protoreflect.Fixed32Kind:  "fixed32",
-	protoreflect.FloatKind:    "fixed32",
-	protoreflect.Sfixed64Kind: "fixed64",
-	protoreflect.Fixed64Kind:  "fixed64",
-	protoreflect.DoubleKind:   "fixed64",
-	protoreflect.StringKind:   "bytes",
-	protoreflect.BytesKind:    "bytes",
-	protoreflect.MessageKind:  "bytes",
-	protoreflect.GroupKind:    "group",
-}
-
 func fieldJSONTag(field *protogen.Field) string {
 	return string(field.Desc.Name()) + ",omitempty"
 }
diff --git a/internal/encoding/tag/tag.go b/internal/encoding/tag/tag.go
new file mode 100644
index 0000000..5c5ba5b
--- /dev/null
+++ b/internal/encoding/tag/tag.go
@@ -0,0 +1,303 @@
+// 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 tag marshals and unmarshals the legacy struct tags as generated
+// by historical versions of protoc-gen-go.
+package tag
+
+import (
+	"fmt"
+	"math"
+	"reflect"
+	"strconv"
+	"strings"
+
+	protoV1 "github.com/golang/protobuf/proto"
+	descriptorV1 "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	ptext "github.com/golang/protobuf/v2/internal/encoding/text"
+	pref "github.com/golang/protobuf/v2/reflect/protoreflect"
+	ptype "github.com/golang/protobuf/v2/reflect/prototype"
+)
+
+var byteType = reflect.TypeOf(byte(0))
+
+// Unmarshal decodes the tag into a prototype.Field.
+//
+// The goType is needed to determine the original protoreflect.Kind since the
+// tag does not record sufficient information to determine that.
+// The type is the underlying field type (e.g., a repeated field may be
+// represented by []T, but the Go type passed in is just T).
+// This does not populate the EnumType or MessageType (except for weak message).
+//
+// This function is a best effort attempt; parsing errors are ignored.
+func Unmarshal(tag string, goType reflect.Type) ptype.Field {
+	var f ptype.Field
+	f.Options = &descriptorV1.FieldOptions{
+		Packed: protoV1.Bool(false),
+	}
+	for len(tag) > 0 {
+		i := strings.IndexByte(tag, ',')
+		if i < 0 {
+			i = len(tag)
+		}
+		switch s := tag[:i]; {
+		case strings.HasPrefix(s, "name="):
+			f.Name = pref.Name(s[len("name="):])
+		case strings.Trim(s, "0123456789") == "":
+			n, _ := strconv.ParseUint(s, 10, 32)
+			f.Number = pref.FieldNumber(n)
+		case s == "opt":
+			f.Cardinality = pref.Optional
+		case s == "req":
+			f.Cardinality = pref.Required
+		case s == "rep":
+			f.Cardinality = pref.Repeated
+		case s == "varint":
+			switch goType.Kind() {
+			case reflect.Bool:
+				f.Kind = pref.BoolKind
+			case reflect.Int32:
+				f.Kind = pref.Int32Kind
+			case reflect.Int64:
+				f.Kind = pref.Int64Kind
+			case reflect.Uint32:
+				f.Kind = pref.Uint32Kind
+			case reflect.Uint64:
+				f.Kind = pref.Uint64Kind
+			}
+		case s == "zigzag32":
+			if goType.Kind() == reflect.Int32 {
+				f.Kind = pref.Sint32Kind
+			}
+		case s == "zigzag64":
+			if goType.Kind() == reflect.Int64 {
+				f.Kind = pref.Sint64Kind
+			}
+		case s == "fixed32":
+			switch goType.Kind() {
+			case reflect.Int32:
+				f.Kind = pref.Sfixed32Kind
+			case reflect.Uint32:
+				f.Kind = pref.Fixed32Kind
+			case reflect.Float32:
+				f.Kind = pref.FloatKind
+			}
+		case s == "fixed64":
+			switch goType.Kind() {
+			case reflect.Int64:
+				f.Kind = pref.Sfixed64Kind
+			case reflect.Uint64:
+				f.Kind = pref.Fixed64Kind
+			case reflect.Float64:
+				f.Kind = pref.DoubleKind
+			}
+		case s == "bytes":
+			switch {
+			case goType.Kind() == reflect.String:
+				f.Kind = pref.StringKind
+			case goType.Kind() == reflect.Slice && goType.Elem() == byteType:
+				f.Kind = pref.BytesKind
+			default:
+				f.Kind = pref.MessageKind
+			}
+		case s == "group":
+			f.Kind = pref.GroupKind
+		case strings.HasPrefix(s, "enum="):
+			f.Kind = pref.EnumKind
+		case strings.HasPrefix(s, "json="):
+			f.JSONName = s[len("json="):]
+		case s == "packed":
+			*f.Options.Packed = true
+		case strings.HasPrefix(s, "weak="):
+			f.Options.Weak = protoV1.Bool(true)
+			f.MessageType = ptype.PlaceholderMessage(pref.FullName(s[len("weak="):]))
+		case strings.HasPrefix(s, "def="):
+			// The default tag is special in that everything afterwards is the
+			// default regardless of the presence of commas.
+			s, i = tag[len("def="):], len(tag)
+
+			// Defaults are parsed last, so Kind is populated.
+			switch f.Kind {
+			case pref.BoolKind:
+				switch s {
+				case "1":
+					f.Default = pref.ValueOf(true)
+				case "0":
+					f.Default = pref.ValueOf(false)
+				}
+			case pref.EnumKind:
+				n, _ := strconv.ParseInt(s, 10, 32)
+				f.Default = pref.ValueOf(pref.EnumNumber(n))
+			case pref.Int32Kind, pref.Sint32Kind, pref.Sfixed32Kind:
+				n, _ := strconv.ParseInt(s, 10, 32)
+				f.Default = pref.ValueOf(int32(n))
+			case pref.Int64Kind, pref.Sint64Kind, pref.Sfixed64Kind:
+				n, _ := strconv.ParseInt(s, 10, 64)
+				f.Default = pref.ValueOf(int64(n))
+			case pref.Uint32Kind, pref.Fixed32Kind:
+				n, _ := strconv.ParseUint(s, 10, 32)
+				f.Default = pref.ValueOf(uint32(n))
+			case pref.Uint64Kind, pref.Fixed64Kind:
+				n, _ := strconv.ParseUint(s, 10, 64)
+				f.Default = pref.ValueOf(uint64(n))
+			case pref.FloatKind, pref.DoubleKind:
+				n, _ := strconv.ParseFloat(s, 64)
+				switch s {
+				case "nan":
+					n = math.NaN()
+				case "inf":
+					n = math.Inf(+1)
+				case "-inf":
+					n = math.Inf(-1)
+				}
+				if f.Kind == pref.FloatKind {
+					f.Default = pref.ValueOf(float32(n))
+				} else {
+					f.Default = pref.ValueOf(float64(n))
+				}
+			case pref.StringKind:
+				f.Default = pref.ValueOf(string(s))
+			case pref.BytesKind:
+				// The default value is in escaped form (C-style).
+				// TODO: Export unmarshalString in the text package to avoid this hack.
+				v, err := ptext.Unmarshal([]byte(`["` + s + `"]:0`))
+				if err == nil && len(v.Message()) == 1 {
+					s := v.Message()[0][0].String()
+					f.Default = pref.ValueOf([]byte(s))
+				}
+			}
+		}
+		tag = strings.TrimPrefix(tag[i:], ",")
+	}
+
+	// The generator uses the group message name instead of the field name.
+	// We obtain the real field name by lowercasing the group name.
+	if f.Kind == pref.GroupKind {
+		f.Name = pref.Name(strings.ToLower(string(f.Name)))
+	}
+	return f
+}
+
+// Marshal encodes the protoreflect.FieldDescriptor as a tag.
+//
+// The enumName must be provided if the kind is an enum.
+// Historically, the formulation of the enum "name" was the proto package
+// dot-concatenated with the generated Go identifier for the enum type.
+// Depending on the context on how Marshal is called, there are different ways
+// through which that information is determined. As such it is the caller's
+// responsibility to provide a function to obtain that information.
+func Marshal(fd pref.FieldDescriptor, enumName string) string {
+	var tag []string
+	switch fd.Kind() {
+	case pref.BoolKind, pref.EnumKind, pref.Int32Kind, pref.Uint32Kind, pref.Int64Kind, pref.Uint64Kind:
+		tag = append(tag, "varint")
+	case pref.Sint32Kind:
+		tag = append(tag, "zigzag32")
+	case pref.Sint64Kind:
+		tag = append(tag, "zigzag64")
+	case pref.Sfixed32Kind, pref.Fixed32Kind, pref.FloatKind:
+		tag = append(tag, "fixed32")
+	case pref.Sfixed64Kind, pref.Fixed64Kind, pref.DoubleKind:
+		tag = append(tag, "fixed64")
+	case pref.StringKind, pref.BytesKind, pref.MessageKind:
+		tag = append(tag, "bytes")
+	case pref.GroupKind:
+		tag = append(tag, "group")
+	}
+	tag = append(tag, strconv.Itoa(int(fd.Number())))
+	switch fd.Cardinality() {
+	case pref.Optional:
+		tag = append(tag, "opt")
+	case pref.Required:
+		tag = append(tag, "req")
+	case pref.Repeated:
+		tag = append(tag, "rep")
+	}
+	if fd.IsPacked() {
+		tag = append(tag, "packed")
+	}
+	// TODO: Weak fields?
+	name := string(fd.Name())
+	if fd.Kind() == pref.GroupKind {
+		// The name of the FieldDescriptor for a group field is
+		// lowercased. To find the original capitalization, we
+		// look in the field's MessageType.
+		name = string(fd.MessageType().Name())
+	}
+	tag = append(tag, "name="+name)
+	if jsonName := fd.JSONName(); jsonName != "" && jsonName != name {
+		tag = append(tag, "json="+jsonName)
+	}
+	// The previous implementation does not tag extension fields as proto3,
+	// even when the field is defined in a proto3 file. Match that behavior
+	// for consistency.
+	if fd.Syntax() == pref.Proto3 && fd.ExtendedType() == nil {
+		tag = append(tag, "proto3")
+	}
+	if fd.Kind() == pref.EnumKind && enumName != "" {
+		tag = append(tag, "enum="+enumName)
+	}
+	if fd.OneofType() != nil {
+		tag = append(tag, "oneof")
+	}
+	// This must appear last in the tag, since commas in strings aren't escaped.
+	if fd.HasDefault() {
+		var def string
+		switch fd.Kind() {
+		case pref.BoolKind:
+			if fd.Default().Bool() {
+				def = "1"
+			} else {
+				def = "0"
+			}
+		case pref.BytesKind:
+			// Preserve protoc-gen-go's historical output of escaped bytes.
+			// This behavior is buggy, but fixing it makes it impossible to
+			// distinguish between the escaped and unescaped forms.
+			//
+			// To match the exact output of protoc, this is identical to the
+			// CEscape function in strutil.cc of the protoc source code.
+			var b []byte
+			for _, c := range fd.Default().Bytes() {
+				switch c {
+				case '\n':
+					b = append(b, `\n`...)
+				case '\r':
+					b = append(b, `\r`...)
+				case '\t':
+					b = append(b, `\t`...)
+				case '"':
+					b = append(b, `\"`...)
+				case '\'':
+					b = append(b, `\'`...)
+				case '\\':
+					b = append(b, `\\`...)
+				default:
+					if c >= 0x20 && c <= 0x7e { // printable ASCII
+						b = append(b, c)
+					} else {
+						b = append(b, fmt.Sprintf(`\%03o`, c)...)
+					}
+				}
+			}
+			def = string(b)
+		case pref.FloatKind, pref.DoubleKind:
+			f := fd.Default().Float()
+			switch {
+			case math.IsInf(f, -1):
+				def = "-inf"
+			case math.IsInf(f, 1):
+				def = "inf"
+			case math.IsNaN(f):
+				def = "nan"
+			default:
+				def = fmt.Sprint(fd.Default().Interface())
+			}
+		default:
+			def = fmt.Sprint(fd.Default().Interface())
+		}
+		tag = append(tag, "def="+def)
+	}
+	return strings.Join(tag, ",")
+}
diff --git a/internal/encoding/tag/tag_test.go b/internal/encoding/tag/tag_test.go
new file mode 100644
index 0000000..d137350
--- /dev/null
+++ b/internal/encoding/tag/tag_test.go
@@ -0,0 +1,55 @@
+// 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 tag
+
+import (
+	"reflect"
+	"testing"
+
+	pref "github.com/golang/protobuf/v2/reflect/protoreflect"
+	ptype "github.com/golang/protobuf/v2/reflect/prototype"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func Test(t *testing.T) {
+	m := &ptype.StandaloneMessage{
+		Syntax:   pref.Proto3,
+		FullName: "golang.org.example.FooMessage",
+		Fields: []ptype.Field{{
+			Name:        "foo_field",
+			Number:      1337,
+			Cardinality: pref.Repeated,
+			Kind:        pref.BytesKind,
+			JSONName:    "fooField",
+			Default:     pref.ValueOf([]byte("hello, \xde\xad\xbe\xef\n")),
+		}},
+	}
+	md, err := ptype.NewMessage(m)
+	if err != nil {
+		t.Fatalf("unexpected NewMessage error: %v", err)
+	}
+
+	// Marshal test.
+	gotTag := Marshal(md.Fields().Get(0), "")
+	wantTag := `bytes,1337,rep,name=foo_field,json=fooField,proto3,def=hello, \336\255\276\357\n`
+	if gotTag != wantTag {
+		t.Errorf("Marshal() = `%v`, want `%v`", gotTag, wantTag)
+	}
+
+	// Unmarshal test.
+	gotField := Unmarshal(wantTag, reflect.TypeOf([]byte{}))
+	wantField := m.Fields[0]
+	opts := cmp.Options{
+		cmp.Transformer("UnwrapValue", func(x pref.Value) interface{} {
+			return x.Interface()
+		}),
+		cmpopts.IgnoreUnexported(ptype.Field{}),
+		cmpopts.IgnoreFields(ptype.Field{}, "Options"),
+	}
+	if diff := cmp.Diff(wantField, gotField, opts); diff != "" {
+		t.Errorf("Unmarshal() mismatch (-want +got):\n%v", diff)
+	}
+}
diff --git a/internal/impl/legacy_message.go b/internal/impl/legacy_message.go
index 12c1832..a617249 100644
--- a/internal/impl/legacy_message.go
+++ b/internal/impl/legacy_message.go
@@ -6,16 +6,14 @@
 
 import (
 	"fmt"
-	"math"
 	"reflect"
-	"strconv"
 	"strings"
 	"sync"
 	"unicode"
 
 	protoV1 "github.com/golang/protobuf/proto"
 	descriptorV1 "github.com/golang/protobuf/protoc-gen-go/descriptor"
-	"github.com/golang/protobuf/v2/internal/encoding/text"
+	ptag "github.com/golang/protobuf/v2/internal/encoding/tag"
 	pref "github.com/golang/protobuf/v2/reflect/protoreflect"
 	ptype "github.com/golang/protobuf/v2/reflect/prototype"
 )
@@ -174,156 +172,14 @@
 	return ptype.PlaceholderMessage(m.FullName)
 }
 
-func (ms *messageDescSet) parseField(tag, tagKey, tagVal string, t reflect.Type, parent *ptype.StandaloneMessage) (f ptype.Field) {
+func (ms *messageDescSet) parseField(tag, tagKey, tagVal string, goType reflect.Type, parent *ptype.StandaloneMessage) ptype.Field {
+	t := goType
 	isOptional := t.Kind() == reflect.Ptr && t.Elem().Kind() != reflect.Struct
 	isRepeated := t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8
 	if isOptional || isRepeated {
 		t = t.Elem()
 	}
-
-	f.Options = &descriptorV1.FieldOptions{
-		Packed: protoV1.Bool(false),
-	}
-	for len(tag) > 0 {
-		i := strings.IndexByte(tag, ',')
-		if i < 0 {
-			i = len(tag)
-		}
-		switch s := tag[:i]; {
-		case strings.HasPrefix(s, "name="):
-			f.Name = pref.Name(s[len("name="):])
-		case strings.Trim(s, "0123456789") == "":
-			n, _ := strconv.ParseUint(s, 10, 32)
-			f.Number = pref.FieldNumber(n)
-		case s == "opt":
-			f.Cardinality = pref.Optional
-		case s == "req":
-			f.Cardinality = pref.Required
-		case s == "rep":
-			f.Cardinality = pref.Repeated
-		case s == "varint":
-			switch t.Kind() {
-			case reflect.Bool:
-				f.Kind = pref.BoolKind
-			case reflect.Int32:
-				f.Kind = pref.Int32Kind
-			case reflect.Int64:
-				f.Kind = pref.Int64Kind
-			case reflect.Uint32:
-				f.Kind = pref.Uint32Kind
-			case reflect.Uint64:
-				f.Kind = pref.Uint64Kind
-			}
-		case s == "zigzag32":
-			if t.Kind() == reflect.Int32 {
-				f.Kind = pref.Sint32Kind
-			}
-		case s == "zigzag64":
-			if t.Kind() == reflect.Int64 {
-				f.Kind = pref.Sint64Kind
-			}
-		case s == "fixed32":
-			switch t.Kind() {
-			case reflect.Int32:
-				f.Kind = pref.Sfixed32Kind
-			case reflect.Uint32:
-				f.Kind = pref.Fixed32Kind
-			case reflect.Float32:
-				f.Kind = pref.FloatKind
-			}
-		case s == "fixed64":
-			switch t.Kind() {
-			case reflect.Int64:
-				f.Kind = pref.Sfixed64Kind
-			case reflect.Uint64:
-				f.Kind = pref.Fixed64Kind
-			case reflect.Float64:
-				f.Kind = pref.DoubleKind
-			}
-		case s == "bytes":
-			switch {
-			case t.Kind() == reflect.String:
-				f.Kind = pref.StringKind
-			case t.Kind() == reflect.Slice && t.Elem() == byteType:
-				f.Kind = pref.BytesKind
-			default:
-				f.Kind = pref.MessageKind
-			}
-		case s == "group":
-			f.Kind = pref.GroupKind
-		case strings.HasPrefix(s, "enum="):
-			f.Kind = pref.EnumKind
-		case strings.HasPrefix(s, "json="):
-			f.JSONName = s[len("json="):]
-		case s == "packed":
-			*f.Options.Packed = true
-		case strings.HasPrefix(s, "weak="):
-			f.Options.Weak = protoV1.Bool(true)
-			f.MessageType = ptype.PlaceholderMessage(pref.FullName(s[len("weak="):]))
-		case strings.HasPrefix(s, "def="):
-			// The default tag is special in that everything afterwards is the
-			// default regardless of the presence of commas.
-			s, i = tag[len("def="):], len(tag)
-
-			// Defaults are parsed last, so Kind is populated.
-			switch f.Kind {
-			case pref.BoolKind:
-				switch s {
-				case "1":
-					f.Default = pref.ValueOf(true)
-				case "0":
-					f.Default = pref.ValueOf(false)
-				}
-			case pref.EnumKind:
-				n, _ := strconv.ParseInt(s, 10, 32)
-				f.Default = pref.ValueOf(pref.EnumNumber(n))
-			case pref.Int32Kind, pref.Sint32Kind, pref.Sfixed32Kind:
-				n, _ := strconv.ParseInt(s, 10, 32)
-				f.Default = pref.ValueOf(int32(n))
-			case pref.Int64Kind, pref.Sint64Kind, pref.Sfixed64Kind:
-				n, _ := strconv.ParseInt(s, 10, 64)
-				f.Default = pref.ValueOf(int64(n))
-			case pref.Uint32Kind, pref.Fixed32Kind:
-				n, _ := strconv.ParseUint(s, 10, 32)
-				f.Default = pref.ValueOf(uint32(n))
-			case pref.Uint64Kind, pref.Fixed64Kind:
-				n, _ := strconv.ParseUint(s, 10, 64)
-				f.Default = pref.ValueOf(uint64(n))
-			case pref.FloatKind, pref.DoubleKind:
-				n, _ := strconv.ParseFloat(s, 64)
-				switch s {
-				case "nan":
-					n = math.NaN()
-				case "inf":
-					n = math.Inf(+1)
-				case "-inf":
-					n = math.Inf(-1)
-				}
-				if f.Kind == pref.FloatKind {
-					f.Default = pref.ValueOf(float32(n))
-				} else {
-					f.Default = pref.ValueOf(float64(n))
-				}
-			case pref.StringKind:
-				f.Default = pref.ValueOf(string(s))
-			case pref.BytesKind:
-				// The default value is in escaped form (C-style).
-				// TODO: Export unmarshalString in the text package to avoid this hack.
-				v, err := text.Unmarshal([]byte(`["` + s + `"]:0`))
-				if err == nil && len(v.Message()) == 1 {
-					s := v.Message()[0][0].String()
-					f.Default = pref.ValueOf([]byte(s))
-				}
-			}
-		}
-		tag = strings.TrimPrefix(tag[i:], ",")
-	}
-
-	// The generator uses the group message name instead of the field name.
-	// We obtain the real field name by lowercasing the group name.
-	if f.Kind == pref.GroupKind {
-		f.Name = pref.Name(strings.ToLower(string(f.Name)))
-	}
+	f := ptag.Unmarshal(tag, t)
 
 	// Populate EnumType and MessageType.
 	if f.EnumType == nil && f.Kind == pref.EnumKind {