-
Notifications
You must be signed in to change notification settings - Fork 22
/
merge.go
164 lines (143 loc) · 5.71 KB
/
merge.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package proto
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
protov2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/descriptorpb"
)
// MergedFileDescriptors returns a single FileDescriptorSet containing all the
// file descriptors registered with both gogoproto and protoregistry. When
// merging it also performs the following checks:
// - check that all file descriptors' import paths are correct (i.e. match
// their fully-qualified package name). A warning is logged if this check fails.
// - check that if both gogo and protoregistry import the same file descriptor,
// that these are identical under proto.Equal. A warning is logged if this
// check fails. If there is a mismatch, the final merged file descriptor set will contain the
// protoregistry file descriptor, and discard the gogo one.
func MergedFileDescriptors() (*descriptorpb.FileDescriptorSet, error) {
return mergedFileDescriptors(false)
}
// DebugFileDescriptorsMismatch is a helper function to debug file descriptor
// mismatches. It returns an error if there are any mismatches.
func DebugFileDescriptorsMismatch() error {
_, err := mergedFileDescriptors(true)
return err
}
func mergedFileDescriptors(debug bool) (*descriptorpb.FileDescriptorSet, error) {
fds := &descriptorpb.FileDescriptorSet{}
// While combing through the file descriptors, we'll also log any errors
// we encounter.
var (
checkImportErr []string
diffErr []string
)
// Add protoregistry file descriptors to our final file descriptor set.
protoregistry.GlobalFiles.RangeFiles(func(fileDescriptor protoreflect.FileDescriptor) bool {
fd := protodesc.ToFileDescriptorProto(fileDescriptor)
if err := CheckImportPath(fd.GetName(), fd.GetPackage()); err != nil {
checkImportErr = append(checkImportErr, err.Error())
}
fds.File = append(fds.File, protodesc.ToFileDescriptorProto(fileDescriptor))
return true
})
// load gogo proto file descriptors
gogoFds := AllFileDescriptors()
for _, compressedBz := range gogoFds {
rdr, err := gzip.NewReader(bytes.NewReader(compressedBz))
if err != nil {
return nil, err
}
bz, err := io.ReadAll(rdr)
if err != nil {
return nil, err
}
fd := &descriptorpb.FileDescriptorProto{}
err = protov2.Unmarshal(bz, fd)
if err != nil {
return nil, err
}
err = CheckImportPath(fd.GetName(), fd.GetPackage())
if err != nil {
checkImportErr = append(checkImportErr, err.Error())
}
// If it's not in the protoregistry file descriptors, add it.
protoregFd, err := protoregistry.GlobalFiles.FindFileByPath(*fd.Name)
// If we already loaded gogo's file descriptor, compare that the 2
// are strictly equal, or else log a warning.
if err != nil {
// If we have a NotFound error, we add this file descriptor to the
// final file descriptor set.
if errors.Is(err, protoregistry.NotFound) {
fds.File = append(fds.File, fd)
} else {
return nil, err
}
} else {
// If there's a mismatch, we log a warning. If there was no
// mismatch, then we do nothing, and take the protoregistry file
// descriptor as the correct one.
if !protov2.Equal(protodesc.ToFileDescriptorProto(protoregFd), fd) {
diff := cmp.Diff(protodesc.ToFileDescriptorProto(protoregFd), fd, protocmp.Transform())
diffErr = append(diffErr, fmt.Sprintf("Mismatch in %s:\n%s", *fd.Name, diff))
}
}
}
if debug {
errStr := new(bytes.Buffer)
if len(checkImportErr) > 0 {
fmt.Fprintf(errStr, "Got %d file descriptor import path errors:\n\t%s\n", len(checkImportErr), strings.Join(checkImportErr, "\n\t"))
}
if len(diffErr) > 0 {
fmt.Fprintf(errStr, "Got %d file descriptor mismatches. Make sure gogoproto and protoregistry use the same .proto files. '-' lines are from protoregistry, '+' lines from gogo's registry.\n\n\t%s\n", len(diffErr), strings.Join(diffErr, "\n\t"))
}
if errStr.Len() > 0 {
return nil, fmt.Errorf(errStr.String())
}
} else {
// In production, we just log a warning to StdErr with the number of
// linter errors.
if len(checkImportErr) > 0 || len(diffErr) > 0 {
fmt.Fprintf(os.Stderr, "Got %d file descriptor import path errors and %d file descriptor mismatches. Run `proto.DebugFileDescriptorsMismatch` to debug them.\n", len(checkImportErr), len(diffErr))
}
}
slices.SortFunc(fds.File, func(x, y *descriptorpb.FileDescriptorProto) bool {
return *x.Name < *y.Name
})
return fds, nil
}
// MergedRegistry returns a *protoregistry.Files that acts as a single registry
// which contains all the file descriptors registered with both gogoproto and
// protoregistry (the latter taking precendence if there's a mismatch).
func MergedRegistry() (*protoregistry.Files, error) {
fds, err := MergedFileDescriptors()
if err != nil {
return nil, err
}
return protodesc.NewFiles(fds)
}
// CheckImportPath checks that the import path of the given file descriptor
// matches its fully qualified package name. To mimic gogo's old behavior, the
// fdPackage string can be empty.
//
// Example:
// Proto file "google/protobuf/descriptor.proto" should be imported
// from OS path ./google/protobuf/descriptor.proto, relatively to a protoc
// path folder (-I flag).
func CheckImportPath(fdName, fdPackage string) error {
expectedPrefix := strings.ReplaceAll(fdPackage, ".", "/") + "/"
if !strings.HasPrefix(fdName, expectedPrefix) {
return fmt.Errorf("file name %s does not start with expected %s; please make sure your folder structure matches the proto files fully-qualified names", fdName, expectedPrefix)
}
return nil
}