Skip to content

Commit 59e75db

Browse files
authored
Add editions helper functions for resolving features to protoutil (#283)
These helpers, in particular `protoutil.ResolveFeature` and `protoutil.ResolveCustomFeature`, will be used from updated checks for `buf breaking`, which will allow the tool to understand the features-related semantics of the schema. This way, it can correctly report issues with incompatible changes to features in editions source files. And it can also allow changing a file's syntax (like migrating from proto2 or proto3 to editions) as long as there are no actual semantic changes to the schema.
1 parent e8799f7 commit 59e75db

File tree

4 files changed

+707
-6
lines changed

4 files changed

+707
-6
lines changed

internal/editions/editions.go

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import (
2323
"sync"
2424

2525
"google.golang.org/protobuf/encoding/prototext"
26+
"google.golang.org/protobuf/proto"
2627
"google.golang.org/protobuf/reflect/protoreflect"
2728
"google.golang.org/protobuf/reflect/protoregistry"
2829
"google.golang.org/protobuf/types/descriptorpb"
30+
"google.golang.org/protobuf/types/dynamicpb"
2931
)
3032

3133
var (
@@ -77,7 +79,7 @@ var _ HasFeatures = (*descriptorpb.MethodOptions)(nil)
7779
// override. If there is no overridden value, it returns a zero value.
7880
func ResolveFeature(
7981
element protoreflect.Descriptor,
80-
field protoreflect.FieldDescriptor,
82+
fields ...protoreflect.FieldDescriptor,
8183
) (protoreflect.Value, error) {
8284
for {
8385
var features *descriptorpb.FeatureSet
@@ -86,9 +88,25 @@ func ResolveFeature(
8688
features = withFeatures.GetFeatures()
8789
}
8890

89-
msgRef := features.ProtoReflect()
90-
if msgRef.Has(field) {
91-
return msgRef.Get(field), nil
91+
msgRef, err := adaptFeatureSet(features, fields[0])
92+
if err != nil {
93+
return protoreflect.Value{}, err
94+
}
95+
// Navigate the fields to find the value
96+
var val protoreflect.Value
97+
for i, field := range fields {
98+
if i > 0 {
99+
msgRef = val.Message()
100+
}
101+
if !msgRef.Has(field) {
102+
val = protoreflect.Value{}
103+
break
104+
}
105+
val = msgRef.Get(field)
106+
}
107+
if val.IsValid() {
108+
// All fields were set!
109+
return val, nil
92110
}
93111

94112
parent := element.Parent()
@@ -230,3 +248,94 @@ func GetFeatureDefault(edition descriptorpb.Edition, container protoreflect.Mess
230248
}
231249
return empty.Get(feature), nil
232250
}
251+
252+
func adaptFeatureSet(msg *descriptorpb.FeatureSet, field protoreflect.FieldDescriptor) (protoreflect.Message, error) {
253+
msgRef := msg.ProtoReflect()
254+
if field.IsExtension() {
255+
// Extensions can always be used directly with the feature set, even if
256+
// field.ContainingMessage() != FeatureSetDescriptor.
257+
if msgRef.Has(field) || len(msgRef.GetUnknown()) == 0 {
258+
return msgRef, nil
259+
}
260+
// The field is not present, but the message has unrecognized values. So
261+
// let's try to parse the unrecognized bytes, just in case they contain
262+
// this extension.
263+
temp := &descriptorpb.FeatureSet{}
264+
unmarshaler := prototext.UnmarshalOptions{
265+
AllowPartial: true,
266+
Resolver: resolverForExtension{field},
267+
}
268+
if err := unmarshaler.Unmarshal(msgRef.GetUnknown(), temp); err != nil {
269+
return nil, fmt.Errorf("failed to parse unrecognized fields of FeatureSet: %w", err)
270+
}
271+
return temp.ProtoReflect(), nil
272+
}
273+
274+
if field.ContainingMessage() == FeatureSetDescriptor {
275+
// Known field, not dynamically generated. Can directly use with the feature set.
276+
return msgRef, nil
277+
}
278+
279+
// If we get here, we have a dynamic field descriptor. We want to copy its
280+
// value into a dynamic message, which requires marshalling/unmarshalling.
281+
msgField := FeatureSetDescriptor.Fields().ByNumber(field.Number())
282+
// We only need to copy over the unrecognized bytes (if any)
283+
// and the same field (if present).
284+
data := msgRef.GetUnknown()
285+
if msgField != nil && msgRef.Has(msgField) {
286+
subset := &descriptorpb.FeatureSet{}
287+
subset.ProtoReflect().Set(msgField, msgRef.Get(msgField))
288+
fieldBytes, err := proto.MarshalOptions{AllowPartial: true}.Marshal(subset)
289+
if err != nil {
290+
return nil, fmt.Errorf("failed to marshal FeatureSet field %s to bytes: %w", field.Name(), err)
291+
}
292+
data = append(data, fieldBytes...)
293+
}
294+
if len(data) == 0 {
295+
// No relevant data to copy over, so we can just return
296+
// a zero value message
297+
return dynamicpb.NewMessageType(field.ContainingMessage()).Zero(), nil
298+
}
299+
300+
other := dynamicpb.NewMessage(field.ContainingMessage())
301+
// We don't need to use a resolver for this step because we know that
302+
// field is not an extension. And features are not allowed to themselves
303+
// have extensions.
304+
if err := (proto.UnmarshalOptions{AllowPartial: true}).Unmarshal(data, other); err != nil {
305+
return nil, fmt.Errorf("failed to marshal FeatureSet field %s to bytes: %w", field.Name(), err)
306+
}
307+
return other, nil
308+
}
309+
310+
type resolverForExtension struct {
311+
ext protoreflect.ExtensionDescriptor
312+
}
313+
314+
func (r resolverForExtension) FindMessageByName(_ protoreflect.FullName) (protoreflect.MessageType, error) {
315+
return nil, protoregistry.NotFound
316+
}
317+
318+
func (r resolverForExtension) FindMessageByURL(_ string) (protoreflect.MessageType, error) {
319+
return nil, protoregistry.NotFound
320+
}
321+
322+
func (r resolverForExtension) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
323+
if field == r.ext.FullName() {
324+
return asExtensionType(r.ext), nil
325+
}
326+
return nil, protoregistry.NotFound
327+
}
328+
329+
func (r resolverForExtension) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
330+
if message == r.ext.ContainingMessage().FullName() && field == r.ext.Number() {
331+
return asExtensionType(r.ext), nil
332+
}
333+
return nil, protoregistry.NotFound
334+
}
335+
336+
func asExtensionType(ext protoreflect.ExtensionDescriptor) protoreflect.ExtensionType {
337+
if xtd, ok := ext.(protoreflect.ExtensionTypeDescriptor); ok {
338+
return xtd.Type()
339+
}
340+
return dynamicpb.NewExtensionType(ext)
341+
}

protoutil/editions.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2020-2024 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package protoutil
16+
17+
import (
18+
"fmt"
19+
20+
"google.golang.org/protobuf/reflect/protoreflect"
21+
"google.golang.org/protobuf/types/descriptorpb"
22+
"google.golang.org/protobuf/types/dynamicpb"
23+
24+
"github.com/bufbuild/protocompile/internal/editions"
25+
)
26+
27+
// GetFeatureDefault gets the default value for the given feature and the given
28+
// edition. The given feature must represent a field of the google.protobuf.FeatureSet
29+
// message and must not be an extension.
30+
//
31+
// If the given field is from a dynamically built descriptor (i.e. it's containing
32+
// message descriptor is different from the linked-in descriptor for
33+
// [*descriptorpb.FeatureSet]), the returned value may be a dynamic value. In such
34+
// cases, the value may not be directly usable using [protoreflect.Message.Set] with
35+
// an instance of [*descriptorpb.FeatureSet] and must instead be used with a
36+
// [*dynamicpb.Message].
37+
//
38+
// To get the default value of a custom feature, use [GetCustomFeatureDefault]
39+
// instead.
40+
func GetFeatureDefault(edition descriptorpb.Edition, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
41+
if feature.ContainingMessage().FullName() != editions.FeatureSetDescriptor.FullName() {
42+
return protoreflect.Value{}, fmt.Errorf("feature %s is a field of %s but should be a field of %s",
43+
feature.Name(), feature.ContainingMessage().FullName(), editions.FeatureSetDescriptor.FullName())
44+
}
45+
var msgType protoreflect.MessageType
46+
if feature.ContainingMessage() == editions.FeatureSetDescriptor {
47+
msgType = editions.FeatureSetType
48+
} else {
49+
msgType = dynamicpb.NewMessageType(feature.ContainingMessage())
50+
}
51+
return editions.GetFeatureDefault(edition, msgType, feature)
52+
}
53+
54+
// GetCustomFeatureDefault gets the default value for the given custom feature
55+
// and given edition. A custom feature is a field whose containing message is the
56+
// type of an extension field of google.protobuf.FeatureSet. The given extension
57+
// describes that extension field and message type. The given feature must be a
58+
// field of that extension's message type.
59+
func GetCustomFeatureDefault(edition descriptorpb.Edition, extension protoreflect.ExtensionType, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
60+
extDesc := extension.TypeDescriptor()
61+
if extDesc.ContainingMessage().FullName() != editions.FeatureSetDescriptor.FullName() {
62+
return protoreflect.Value{}, fmt.Errorf("extension %s does not extend %s", extDesc.FullName(), editions.FeatureSetDescriptor.FullName())
63+
}
64+
if extDesc.Message() == nil {
65+
return protoreflect.Value{}, fmt.Errorf("extensions of %s should be messages; %s is instead %s",
66+
editions.FeatureSetDescriptor.FullName(), extDesc.FullName(), extDesc.Kind().String())
67+
}
68+
if feature.IsExtension() {
69+
return protoreflect.Value{}, fmt.Errorf("feature %s is an extension, but feature extension %s may not itself have extensions",
70+
feature.FullName(), extDesc.FullName())
71+
}
72+
if feature.ContainingMessage().FullName() != extDesc.Message().FullName() {
73+
return protoreflect.Value{}, fmt.Errorf("feature %s is a field of %s but should be a field of %s",
74+
feature.Name(), feature.ContainingMessage().FullName(), extDesc.Message().FullName())
75+
}
76+
if feature.ContainingMessage() != extDesc.Message() {
77+
return protoreflect.Value{}, fmt.Errorf("feature %s has a different message descriptor from the given extension type for %s",
78+
feature.Name(), extDesc.Message().FullName())
79+
}
80+
return editions.GetFeatureDefault(edition, extension.Zero().Message().Type(), feature)
81+
}
82+
83+
// ResolveFeature resolves a feature for the given descriptor.
84+
//
85+
// If the given element is in a proto2 or proto3 syntax file, this skips
86+
// resolution and just returns the relevant default (since such files are not
87+
// allowed to override features). If neither the given element nor any of its
88+
// ancestors override the given feature, the relevant default is returned.
89+
//
90+
// This has the same caveat as GetFeatureDefault if the given feature is from a
91+
// dynamically built descriptor.
92+
func ResolveFeature(element protoreflect.Descriptor, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
93+
edition := editions.GetEdition(element)
94+
defaultVal, err := GetFeatureDefault(edition, feature)
95+
if err != nil {
96+
return protoreflect.Value{}, err
97+
}
98+
return resolveFeature(edition, defaultVal, element, feature)
99+
}
100+
101+
// ResolveCustomFeature resolves a custom feature for the given extension and
102+
// field descriptor.
103+
//
104+
// The given extension must be an extension of google.protobuf.FeatureSet that
105+
// represents a non-repeated message value. The given feature is a field in
106+
// that extension's message type.
107+
//
108+
// If the given element is in a proto2 or proto3 syntax file, this skips
109+
// resolution and just returns the relevant default (since such files are not
110+
// allowed to override features). If neither the given element nor any of its
111+
// ancestors override the given feature, the relevant default is returned.
112+
func ResolveCustomFeature(element protoreflect.Descriptor, extension protoreflect.ExtensionType, feature protoreflect.FieldDescriptor) (protoreflect.Value, error) {
113+
edition := editions.GetEdition(element)
114+
defaultVal, err := GetCustomFeatureDefault(edition, extension, feature)
115+
if err != nil {
116+
return protoreflect.Value{}, err
117+
}
118+
return resolveFeature(edition, defaultVal, element, extension.TypeDescriptor(), feature)
119+
}
120+
121+
func resolveFeature(
122+
edition descriptorpb.Edition,
123+
defaultVal protoreflect.Value,
124+
element protoreflect.Descriptor,
125+
fields ...protoreflect.FieldDescriptor,
126+
) (protoreflect.Value, error) {
127+
if edition == descriptorpb.Edition_EDITION_PROTO2 || edition == descriptorpb.Edition_EDITION_PROTO3 {
128+
// these syntax levels can't specify features, so we can short-circuit the search
129+
// through the descriptor hierarchy for feature overrides
130+
return defaultVal, nil
131+
}
132+
val, err := editions.ResolveFeature(element, fields...)
133+
if err != nil {
134+
return protoreflect.Value{}, err
135+
}
136+
if val.IsValid() {
137+
return val, nil
138+
}
139+
return defaultVal, nil
140+
}

0 commit comments

Comments
 (0)