protobuf-go/internal/impl/legacy_message.go
Joe Tsai 851185dae3 internal/impl: support aberrant enums and messages
Aberrant messages are hand-crafted messages that happen to work because
they use the same struct tags that generated code emits.
This happens to work in v1, but is unspecified behavior and entirely outside
the compatibility promise.

Support for this was added early on in the history of the v2 implementation,
but entirely untested. It was removed in CL/182360 to reduce the
technical debt of the legacy implementation. Unfortunately, sufficient number
of targets do rely on this aberrant support, so it is being added back.

The logic being added is essentially the same thing as the previous logic,
but ported to use internal/filedesc instead of the now deleted
internal/prototype package.

Change-Id: Ib5cab3e90480825b9615db358044ce05a14b05bd
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/184517
Reviewed-by: Damien Neil <dneil@google.com>
2019-07-01 21:18:15 +00:00

327 lines
11 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 impl
import (
"fmt"
"reflect"
"strings"
"sync"
"unicode"
"google.golang.org/protobuf/internal/descopts"
ptag "google.golang.org/protobuf/internal/encoding/tag"
"google.golang.org/protobuf/internal/filedesc"
"google.golang.org/protobuf/reflect/protoreflect"
pref "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/prototype"
)
// legacyWrapMessage wraps v as a protoreflect.ProtoMessage,
// where v must be a *struct kind and not implement the v2 API already.
func legacyWrapMessage(v reflect.Value) pref.ProtoMessage {
mt := legacyLoadMessageInfo(v.Type())
return mt.MessageOf(v.Interface()).Interface()
}
var legacyMessageTypeCache sync.Map // map[reflect.Type]*MessageInfo
// legacyLoadMessageInfo dynamically loads a *MessageInfo for t,
// where t must be a *struct kind and not implement the v2 API already.
func legacyLoadMessageInfo(t reflect.Type) *MessageInfo {
// Fast-path: check if a MessageInfo is cached for this concrete type.
if mt, ok := legacyMessageTypeCache.Load(t); ok {
return mt.(*MessageInfo)
}
// Slow-path: derive message descriptor and initialize MessageInfo.
md := LegacyLoadMessageDesc(t)
mt := new(MessageInfo)
mt.GoType = t
mt.PBType = &prototype.Message{
MessageDescriptor: md,
NewMessage: func() pref.Message {
return mt.MessageOf(reflect.New(t.Elem()).Interface())
},
}
if mt, ok := legacyMessageTypeCache.LoadOrStore(t, mt); ok {
return mt.(*MessageInfo)
}
return mt
}
var legacyMessageDescCache sync.Map // map[reflect.Type]protoreflect.MessageDescriptor
// LegacyLoadMessageDesc returns an MessageDescriptor derived from the Go type,
// which must be a *struct kind and not implement the v2 API already.
//
// This is exported for testing purposes.
func LegacyLoadMessageDesc(t reflect.Type) pref.MessageDescriptor {
return legacyLoadMessageDesc(t, true)
}
func legacyLoadMessageDesc(t reflect.Type, finalized bool) pref.MessageDescriptor {
// Fast-path: check if a MessageDescriptor is cached for this concrete type.
if mi, ok := legacyMessageDescCache.Load(t); ok {
return mi.(pref.MessageDescriptor)
}
// Slow-path: initialize MessageDescriptor from the raw descriptor.
mv := reflect.New(t.Elem()).Interface()
if _, ok := mv.(pref.ProtoMessage); ok {
panic(fmt.Sprintf("%v already implements proto.Message", t))
}
mdV1, ok := mv.(messageV1)
if !ok {
return aberrantLoadMessageDesc(t, finalized)
}
b, idxs := mdV1.Descriptor()
md := legacyLoadFileDesc(b).Messages().Get(idxs[0])
for _, i := range idxs[1:] {
md = md.Messages().Get(i)
}
if md, ok := legacyMessageDescCache.LoadOrStore(t, md); ok {
return md.(protoreflect.MessageDescriptor)
}
return md
}
var aberrantMessageDescCache sync.Map // map[reflect.Type]aberrantMessageDesc
// aberrantMessageDesc is a tuple containing a MessageDescriptor and a channel
// to signal whether the descriptor is initialized. For external lookups,
// we must ensure that the descriptor is fully initialized. For internal lookups
// to resolve cycles, we only need to obtain the descriptor reference.
type aberrantMessageDesc struct {
desc protoreflect.MessageDescriptor
done chan struct{} // closed when desc is fully initialized
}
// aberrantLoadEnumDesc returns an EnumDescriptor derived from the Go type,
// which must not implement protoreflect.ProtoMessage or messageV1.
//
// This is a best-effort derivation of the message descriptor using the protobuf
// tags on the struct fields.
//
// The finalized flag determines whether the returned message descriptor must
// be fully initialized.
func aberrantLoadMessageDesc(t reflect.Type, finalized bool) pref.MessageDescriptor {
// Fast-path: check if an MessageDescriptor is cached for this concrete type.
if mdi, ok := aberrantMessageDescCache.Load(t); ok {
if finalized {
<-mdi.(aberrantMessageDesc).done
}
return mdi.(aberrantMessageDesc).desc
}
// Medium-path: create an initial descriptor and cache it immediately,
// so that cyclic references can be resolved. Each descriptor is paired
// with a channel to signal when the descriptor is fully initialized.
md := &filedesc.Message{L2: new(filedesc.MessageL2)}
mdi := aberrantMessageDesc{desc: md, done: make(chan struct{})}
if mdi, ok := aberrantMessageDescCache.LoadOrStore(t, mdi); ok {
if finalized {
<-mdi.(aberrantMessageDesc).done
}
return mdi.(aberrantMessageDesc).desc
}
defer func() { close(mdi.done) }()
// Slow-path: construct a descriptor from the Go struct type (best-effort).
md.L0.FullName = aberrantDeriveFullName(t.Elem())
md.L0.ParentFile = filedesc.SurrogateProto2
// If possible, use the custom protobuf name specified on the type.
fn, ok := t.MethodByName("XXX_MessageName")
if !ok {
fn, ok = t.Elem().MethodByName("XXX_MessageName")
}
if ok {
v := fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[0]
if s := pref.FullName(v.String()); s.IsValid() {
md.L0.FullName = s
}
}
// Try to determine if the message is using proto3 by checking scalars.
for i := 0; i < t.Elem().NumField(); i++ {
f := t.Elem().Field(i)
if tag := f.Tag.Get("protobuf"); tag != "" {
switch f.Type.Kind() {
case reflect.Bool, reflect.Int32, reflect.Int64, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String:
md.L0.ParentFile = filedesc.SurrogateProto3
}
for _, s := range strings.Split(tag, ",") {
if s == "proto3" {
md.L0.ParentFile = filedesc.SurrogateProto3
}
}
}
}
// Obtain a list of oneof wrapper types.
var oneofWrappers []reflect.Type
if fn, ok := t.MethodByName("XXX_OneofFuncs"); ok {
vs := fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[3]
for _, v := range vs.Interface().([]interface{}) {
oneofWrappers = append(oneofWrappers, reflect.TypeOf(v))
}
}
if fn, ok := t.MethodByName("XXX_OneofWrappers"); ok {
vs := fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[0]
for _, v := range vs.Interface().([]interface{}) {
oneofWrappers = append(oneofWrappers, reflect.TypeOf(v))
}
}
// Obtain a list of the extension ranges.
if fn, ok := t.MethodByName("ExtensionRangeArray"); ok {
vs := fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[0]
for i := 0; i < vs.Len(); i++ {
v := vs.Index(i)
md.L2.ExtensionRanges.List = append(md.L2.ExtensionRanges.List, [2]pref.FieldNumber{
pref.FieldNumber(v.FieldByName("Start").Int()),
pref.FieldNumber(v.FieldByName("End").Int() + 1),
})
md.L2.ExtensionRangeOptions = append(md.L2.ExtensionRangeOptions, nil)
}
}
// Derive the message fields by inspecting the struct fields.
for i := 0; i < t.Elem().NumField(); i++ {
f := t.Elem().Field(i)
if tag := f.Tag.Get("protobuf"); tag != "" {
tagKey := f.Tag.Get("protobuf_key")
tagVal := f.Tag.Get("protobuf_val")
aberrantAppendField(md, f.Type, tag, tagKey, tagVal)
}
if tag := f.Tag.Get("protobuf_oneof"); tag != "" {
n := len(md.L2.Oneofs.List)
md.L2.Oneofs.List = append(md.L2.Oneofs.List, filedesc.Oneof{})
od := &md.L2.Oneofs.List[n]
od.L0.FullName = md.FullName().Append(pref.Name(tag))
od.L0.ParentFile = md.L0.ParentFile
od.L0.Parent = md
od.L0.Index = n
for _, t := range oneofWrappers {
if t.Implements(f.Type) {
f := t.Elem().Field(0)
if tag := f.Tag.Get("protobuf"); tag != "" {
aberrantAppendField(md, f.Type, tag, "", "")
fd := &md.L2.Fields.List[len(md.L2.Fields.List)-1]
fd.L1.ContainingOneof = od
od.L1.Fields.List = append(od.L1.Fields.List, fd)
}
}
}
}
}
// TODO: Use custom Marshal/Unmarshal methods for the fast-path?
return md
}
func aberrantAppendField(md *filedesc.Message, goType reflect.Type, tag, tagKey, tagVal string) {
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()
}
fd := ptag.Unmarshal(tag, t, placeholderEnumValues{}).(*filedesc.Field)
// Append field descriptor to the message.
n := len(md.L2.Fields.List)
md.L2.Fields.List = append(md.L2.Fields.List, *fd)
fd = &md.L2.Fields.List[n]
fd.L0.FullName = md.FullName().Append(fd.Name())
fd.L0.ParentFile = md.L0.ParentFile
fd.L0.Parent = md
fd.L0.Index = n
if fd.L1.IsWeak || fd.L1.HasPacked {
fd.L1.Options = func() pref.ProtoMessage {
opts := descopts.Field.ProtoReflect().New()
if fd.L1.IsWeak {
opts.Set(opts.Descriptor().Fields().ByName("weak"), protoreflect.ValueOf(true))
}
if fd.L1.HasPacked {
opts.Set(opts.Descriptor().Fields().ByName("packed"), protoreflect.ValueOf(fd.L1.IsPacked))
}
return opts.Interface()
}
}
// Populate Enum and Message.
if fd.Enum() == nil && fd.Kind() == pref.EnumKind {
switch v := reflect.Zero(t).Interface().(type) {
case pref.Enum:
fd.L1.Enum = v.Descriptor()
default:
fd.L1.Enum = LegacyLoadEnumDesc(t)
}
}
if fd.Message() == nil && (fd.Kind() == pref.MessageKind || fd.Kind() == pref.GroupKind) {
switch v := reflect.Zero(t).Interface().(type) {
case pref.ProtoMessage:
fd.L1.Message = v.ProtoReflect().Descriptor()
default:
if t.Kind() == reflect.Map {
n := len(md.L1.Messages.List)
md.L1.Messages.List = append(md.L1.Messages.List, filedesc.Message{L2: new(filedesc.MessageL2)})
md2 := &md.L1.Messages.List[n]
md2.L0.FullName = md.FullName().Append(aberrantMapEntryName(fd.Name()))
md2.L0.ParentFile = md.L0.ParentFile
md2.L0.Parent = md
md2.L0.Index = n
md2.L2.IsMapEntry = true
md2.L2.Options = func() pref.ProtoMessage {
opts := descopts.Message.ProtoReflect().New()
opts.Set(opts.Descriptor().Fields().ByName("map_entry"), protoreflect.ValueOf(true))
return opts.Interface()
}
aberrantAppendField(md2, t.Key(), tagKey, "", "")
aberrantAppendField(md2, t.Elem(), tagVal, "", "")
fd.L1.Message = md2
break
}
fd.L1.Message = aberrantLoadMessageDesc(t, false)
}
}
}
type placeholderEnumValues struct {
protoreflect.EnumValueDescriptors
}
func (placeholderEnumValues) ByNumber(n pref.EnumNumber) pref.EnumValueDescriptor {
return filedesc.PlaceholderEnumValue(pref.FullName(fmt.Sprintf("UNKNOWN_%d", n)))
}
// aberrantMapEntryName derives the name for a map entry message.
// See protoc v3.8.0: src/google/protobuf/descriptor.cc:254-276,6057
func aberrantMapEntryName(s pref.Name) pref.Name {
var b []byte
upperNext := true
for _, c := range s {
switch {
case c == '_':
upperNext = true
case upperNext:
b = append(b, byte(unicode.ToUpper(c)))
upperNext = false
default:
b = append(b, byte(c))
}
}
b = append(b, "Entry"...)
return pref.Name(b)
}