mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
f6092b9ce4
This change adds a new flag for emitting plan bundles in the new protobuf wire format. `opa build --format=json|proto`. With `--format=proto`, the bundle contains `/plan.pb` and `/.manifest.pb` in place of `/plan.json`and `/.manifest`. Bundle Reader auto-detects both forms; mixed-format bundles are rejected at read, merge, and write time. Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
612 lines
20 KiB
Go
612 lines
20 KiB
Go
// Copyright 2026 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package protoschemacheck verifies that a hand-authored .proto file
|
|
// stays consistent with the Go types it mirrors. Powers the proto
|
|
// consistency tests for v1/bundle/manifest.proto and v1/ir/plan.proto.
|
|
package protoschemacheck
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bufbuild/protocompile"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
)
|
|
|
|
// loadTimeout caps how long we'll wait for protocompile to parse the
|
|
// proto file so a pathological input fails fast.
|
|
const loadTimeout = 30 * time.Second
|
|
|
|
// Spec describes a consistency check between a .proto file and a set of
|
|
// Go types.
|
|
type Spec struct {
|
|
// ProtoPath is the .proto file relative to the test's working dir.
|
|
ProtoPath string
|
|
// ImportPaths are extra search dirs for imports; "." is added by default.
|
|
ImportPaths []string
|
|
// Messages enumerates proto messages to validate. Every message
|
|
// declared in the .proto (including nested) must appear here or in
|
|
// OpaqueMessages.
|
|
Messages []MessageSpec
|
|
// OpaqueMessages names polymorphic-union envelope messages with no
|
|
// direct Go counterpart (content is checked via Oneofs). Every entry
|
|
// must pair with an OneofSpec.
|
|
OpaqueMessages []string
|
|
// Oneofs enumerates polymorphic-union checks.
|
|
Oneofs []OneofSpec
|
|
}
|
|
|
|
// MessageSpec asserts that proto message Name matches GoType.
|
|
type MessageSpec struct {
|
|
Name string
|
|
// GoType is the Go struct compared against the proto message.
|
|
// Anonymous (embedded) struct fields are flattened recursively
|
|
// (mirroring encoding/json) unless listed in SkipEmbeddedTypes.
|
|
GoType reflect.Type
|
|
// SkipEmbeddedTypes opts out specific embedded types from flattening
|
|
// (their fields are validated on a different proto message — see
|
|
// the IR plan schema where Stmt bodies embed Location but the proto
|
|
// promotes Location's fields onto the Stmt envelope).
|
|
SkipEmbeddedTypes []reflect.Type
|
|
// FieldNameOverride remaps Go-side JSON name → proto field name
|
|
// where the default mapping doesn't apply (e.g. ir.MakeNumberRefStmt's
|
|
// Index Go field maps to proto field "index").
|
|
FieldNameOverride map[string]string
|
|
// OpaqueProtoFields names proto fields whose Go counterpart isn't
|
|
// reflectable into proto (e.g. types.Function fields modeled as
|
|
// google.protobuf.Struct). The check requires the Go field to exist
|
|
// with a structural type — opaque is "no type check feasible", not
|
|
// "no Go field expected".
|
|
OpaqueProtoFields []string
|
|
// SkipGoFields names Go fields (by Go field name, not JSON name)
|
|
// that have no proto counterpart by design — e.g. ir.BuiltinFunc.Decl
|
|
// is intentionally absent from the proto because consumers consult
|
|
// their own registry for builtin signatures.
|
|
SkipGoFields []string
|
|
// SkipProtoFields names proto fields with no Go counterpart by design —
|
|
// wire-form bookkeeping that the Go side carries differently. For
|
|
// example, bundle.Manifest.roots_set distinguishes nil from explicit-
|
|
// empty on the wire; the Go side already carries that as Roots *[]string.
|
|
SkipProtoFields []string
|
|
}
|
|
|
|
// OneofSpec asserts that the named oneof on MessageName has exactly the
|
|
// listed cases with matching Go-side types.
|
|
type OneofSpec struct {
|
|
MessageName string
|
|
OneofName string
|
|
// DiscriminatorToCase maps each JSON-discriminator (runtime kind
|
|
// name) to the proto oneof case name.
|
|
DiscriminatorToCase map[string]string
|
|
// DiscriminatorToGoType maps each discriminator to the Go type
|
|
// implementing the union. Must have the same key set as
|
|
// DiscriminatorToCase. Struct types check by name; scalar types
|
|
// check by reflect.Kind.
|
|
DiscriminatorToGoType map[string]reflect.Type
|
|
}
|
|
|
|
// Run executes the spec, reporting every drift via t.Errorf.
|
|
func Run(t *testing.T, spec Spec) {
|
|
t.Helper()
|
|
if err := validateSpec(spec); err != nil {
|
|
t.Fatalf("invalid spec: %v", err)
|
|
}
|
|
file := loadProto(t, spec.ProtoPath, spec.ImportPaths)
|
|
|
|
declared := map[string]protoreflect.MessageDescriptor{}
|
|
collectDeclared(file.Messages(), declared)
|
|
|
|
covered := map[string]bool{}
|
|
oneofParents := map[string]bool{}
|
|
for _, o := range spec.Oneofs {
|
|
oneofParents[o.MessageName] = true
|
|
}
|
|
// Collect, per message, the names of oneofs covered by an OneofSpec.
|
|
// Fields belonging to those oneofs are validated by checkOneof and
|
|
// must NOT be reported as orphans by checkMessage's field walk.
|
|
coveredOneofs := map[string]map[string]bool{}
|
|
for _, o := range spec.Oneofs {
|
|
if coveredOneofs[o.MessageName] == nil {
|
|
coveredOneofs[o.MessageName] = map[string]bool{}
|
|
}
|
|
coveredOneofs[o.MessageName][o.OneofName] = true
|
|
}
|
|
for _, m := range spec.Messages {
|
|
covered[m.Name] = true
|
|
checkMessage(t, declared, m, coveredOneofs[m.Name])
|
|
}
|
|
for _, name := range spec.OpaqueMessages {
|
|
covered[name] = true
|
|
if _, ok := declared[name]; !ok {
|
|
t.Errorf("opaque message %q listed in spec but not present in %s", name, spec.ProtoPath)
|
|
}
|
|
if !oneofParents[name] {
|
|
t.Errorf("opaque message %q has no matching OneofSpec; OpaqueMessages is for polymorphic envelopes only — add an OneofSpec or move the message into Messages", name)
|
|
}
|
|
if hasMessageSpec(spec, name) {
|
|
t.Errorf("message %q is listed in both Messages and OpaqueMessages — pick one", name)
|
|
}
|
|
}
|
|
for name := range declared {
|
|
if !covered[name] {
|
|
t.Errorf("proto message %q is declared in %s but not covered by the spec; add a MessageSpec or list it in OpaqueMessages", name, spec.ProtoPath)
|
|
}
|
|
}
|
|
|
|
for _, o := range spec.Oneofs {
|
|
checkOneof(t, declared, o)
|
|
}
|
|
}
|
|
|
|
func validateSpec(s Spec) error {
|
|
if s.ProtoPath == "" {
|
|
return errors.New("ProtoPath is required")
|
|
}
|
|
for _, o := range s.Oneofs {
|
|
// EqualFunc with an always-true comparator collapses to a key-set
|
|
// equality check, ignoring the differing value types.
|
|
eq := maps.EqualFunc(o.DiscriminatorToCase, o.DiscriminatorToGoType,
|
|
func(string, reflect.Type) bool { return true })
|
|
if !eq {
|
|
return fmt.Errorf("oneof %s.%s: DiscriminatorToCase and DiscriminatorToGoType have different key sets",
|
|
o.MessageName, o.OneofName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasMessageSpec(s Spec, name string) bool {
|
|
for _, m := range s.Messages {
|
|
if m.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func collectDeclared(msgs protoreflect.MessageDescriptors, out map[string]protoreflect.MessageDescriptor) {
|
|
for i := range msgs.Len() {
|
|
m := msgs.Get(i)
|
|
// Skip synthetic map-entry messages — proto generates them
|
|
// implicitly for map<K,V> fields and they have no user-visible
|
|
// counterpart in either the proto source or the Go types.
|
|
if m.IsMapEntry() {
|
|
continue
|
|
}
|
|
out[string(m.Name())] = m
|
|
collectDeclared(m.Messages(), out)
|
|
}
|
|
}
|
|
|
|
func loadProto(t *testing.T, protoPath string, importPaths []string) protoreflect.FileDescriptor {
|
|
t.Helper()
|
|
paths := append([]string{"."}, importPaths...)
|
|
c := protocompile.Compiler{
|
|
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
|
|
ImportPaths: paths,
|
|
}),
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), loadTimeout)
|
|
defer cancel()
|
|
files, err := c.Compile(ctx, protoPath)
|
|
if err != nil {
|
|
t.Fatalf("compile %s: %v", protoPath, err)
|
|
}
|
|
if len(files) != 1 {
|
|
t.Fatalf("compile %s: expected 1 file, got %d", protoPath, len(files))
|
|
}
|
|
return files[0]
|
|
}
|
|
|
|
func checkMessage(t *testing.T, declared map[string]protoreflect.MessageDescriptor, m MessageSpec, coveredOneofs map[string]bool) {
|
|
t.Helper()
|
|
msg, ok := declared[m.Name]
|
|
if !ok {
|
|
t.Errorf("proto message %q not declared in any input file", m.Name)
|
|
return
|
|
}
|
|
|
|
override := m.FieldNameOverride
|
|
skipEmbedded := map[reflect.Type]bool{}
|
|
for _, et := range m.SkipEmbeddedTypes {
|
|
skipEmbedded[et] = true
|
|
}
|
|
skipGo := map[string]bool{}
|
|
for _, n := range m.SkipGoFields {
|
|
skipGo[n] = true
|
|
}
|
|
|
|
type expected struct {
|
|
protoName string
|
|
goType reflect.Type
|
|
goName string
|
|
}
|
|
var want []expected
|
|
visited := map[reflect.Type]bool{}
|
|
var collect func(rt reflect.Type, qualifier string)
|
|
collect = func(rt reflect.Type, qualifier string) {
|
|
if rt.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
if visited[rt] {
|
|
return
|
|
}
|
|
visited[rt] = true
|
|
for i := range rt.NumField() {
|
|
f := rt.Field(i)
|
|
if !f.IsExported() {
|
|
continue
|
|
}
|
|
if skipGo[f.Name] {
|
|
continue
|
|
}
|
|
if f.Anonymous {
|
|
ft := f.Type
|
|
for ft.Kind() == reflect.Pointer {
|
|
ft = ft.Elem()
|
|
}
|
|
if ft.Kind() == reflect.Struct {
|
|
if skipEmbedded[ft] {
|
|
continue
|
|
}
|
|
collect(ft, qualifier)
|
|
continue
|
|
}
|
|
}
|
|
name, ok := jsonFieldName(f)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if remap, has := override[name]; has {
|
|
name = remap
|
|
}
|
|
want = append(want, expected{
|
|
protoName: name,
|
|
goType: f.Type,
|
|
goName: qualifier + "." + f.Name,
|
|
})
|
|
}
|
|
}
|
|
collect(m.GoType, m.GoType.Name())
|
|
|
|
// Validate SkipGoFields entries point at real Go fields.
|
|
for _, n := range m.SkipGoFields {
|
|
if !goFieldExists(m.GoType, n) {
|
|
t.Errorf("%s: SkipGoFields entry %q does not match any Go field", m.Name, n)
|
|
}
|
|
}
|
|
|
|
opaque := map[string]bool{}
|
|
for _, n := range m.OpaqueProtoFields {
|
|
opaque[n] = true
|
|
if msg.Fields().ByName(protoreflect.Name(n)) == nil {
|
|
t.Errorf("%s: OpaqueProtoFields entry %q does not match any proto field on this message", m.Name, n)
|
|
}
|
|
}
|
|
|
|
skipProto := map[string]bool{}
|
|
for _, n := range m.SkipProtoFields {
|
|
skipProto[n] = true
|
|
if msg.Fields().ByName(protoreflect.Name(n)) == nil {
|
|
t.Errorf("%s: SkipProtoFields entry %q does not match any proto field on this message", m.Name, n)
|
|
}
|
|
}
|
|
|
|
// Validate FieldNameOverride entries point at real Go fields.
|
|
for goJSONName := range override {
|
|
if !goJSONNameExists(m.GoType, goJSONName) {
|
|
t.Errorf("%s: FieldNameOverride key %q does not match any Go field's JSON name", m.Name, goJSONName)
|
|
}
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
for _, w := range want {
|
|
seen[w.protoName] = true
|
|
pf := msg.Fields().ByName(protoreflect.Name(w.protoName))
|
|
if pf == nil {
|
|
t.Errorf("%s: Go field %s maps to proto field %q but %s has no such field; add it with the next available field number", m.Name, w.goName, w.protoName, m.Name)
|
|
continue
|
|
}
|
|
if opaque[w.protoName] {
|
|
// Opaque means "no type check feasible" — but the Go side
|
|
// must still be a structural type. A scalar Go field paired
|
|
// with an opaque proto field is almost certainly a drift bug.
|
|
switch unwrapPointer(w.goType).Kind() {
|
|
case reflect.Bool,
|
|
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
|
reflect.Float32, reflect.Float64,
|
|
reflect.String:
|
|
t.Errorf("%s.%s (Go field %s): proto field is opaque but Go type %s is scalar; opaque is for unreflectable structural types only", m.Name, w.protoName, w.goName, w.goType)
|
|
}
|
|
continue
|
|
}
|
|
if err := checkFieldType(w.goType, pf); err != nil {
|
|
t.Errorf("%s.%s (Go field %s): %v", m.Name, w.protoName, w.goName, err)
|
|
}
|
|
}
|
|
|
|
// Find proto fields that have no Go counterpart.
|
|
for i := range msg.Fields().Len() {
|
|
pf := msg.Fields().Get(i)
|
|
name := string(pf.Name())
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
if opaque[name] {
|
|
continue
|
|
}
|
|
if skipProto[name] {
|
|
continue
|
|
}
|
|
// Fields belonging to a oneof handled by an OneofSpec are
|
|
// validated there, not here. Skip them so the orphan check
|
|
// doesn't double-fire.
|
|
if oo := pf.ContainingOneof(); oo != nil && coveredOneofs[string(oo.Name())] {
|
|
continue
|
|
}
|
|
t.Errorf("%s: proto field %q (number %d) has no corresponding Go field; either remove it (and add `reserved %d`) or add a matching Go field", m.Name, name, pf.Number(), pf.Number())
|
|
}
|
|
}
|
|
|
|
func goJSONNameExists(rt reflect.Type, target string) bool {
|
|
if rt.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
for i := range rt.NumField() {
|
|
f := rt.Field(i)
|
|
if !f.IsExported() {
|
|
continue
|
|
}
|
|
if f.Anonymous {
|
|
ft := f.Type
|
|
for ft.Kind() == reflect.Pointer {
|
|
ft = ft.Elem()
|
|
}
|
|
if ft.Kind() == reflect.Struct && goJSONNameExists(ft, target) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
name, ok := jsonFieldName(f)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if name == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func goFieldExists(rt reflect.Type, target string) bool {
|
|
if rt.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
for i := range rt.NumField() {
|
|
f := rt.Field(i)
|
|
if !f.IsExported() {
|
|
continue
|
|
}
|
|
if f.Name == target {
|
|
return true
|
|
}
|
|
if f.Anonymous {
|
|
ft := f.Type
|
|
for ft.Kind() == reflect.Pointer {
|
|
ft = ft.Elem()
|
|
}
|
|
if ft.Kind() == reflect.Struct && goFieldExists(ft, target) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func checkOneof(t *testing.T, declared map[string]protoreflect.MessageDescriptor, o OneofSpec) {
|
|
t.Helper()
|
|
msg, ok := declared[o.MessageName]
|
|
if !ok {
|
|
t.Errorf("oneof check: proto message %q not declared", o.MessageName)
|
|
return
|
|
}
|
|
oneof := msg.Oneofs().ByName(protoreflect.Name(o.OneofName))
|
|
if oneof == nil {
|
|
t.Errorf("oneof check: %s has no oneof named %q", o.MessageName, o.OneofName)
|
|
return
|
|
}
|
|
|
|
caseFields := map[string]protoreflect.FieldDescriptor{}
|
|
for i := range oneof.Fields().Len() {
|
|
f := oneof.Fields().Get(i)
|
|
caseFields[string(f.Name())] = f
|
|
}
|
|
|
|
discriminators := slices.Sorted(maps.Keys(o.DiscriminatorToCase))
|
|
|
|
expectedCases := map[string]bool{}
|
|
for _, d := range discriminators {
|
|
caseName := o.DiscriminatorToCase[d]
|
|
expectedCases[caseName] = true
|
|
f, ok := caseFields[caseName]
|
|
if !ok {
|
|
t.Errorf("%s.%s: discriminator %q maps to oneof case %q, but no such case exists", o.MessageName, o.OneofName, d, caseName)
|
|
continue
|
|
}
|
|
goType := o.DiscriminatorToGoType[d]
|
|
if goType == nil {
|
|
t.Errorf("%s.%s.%s: discriminator %q has nil Go type — every discriminator must declare a concrete type so the proto kind can be checked", o.MessageName, o.OneofName, caseName, d)
|
|
continue
|
|
}
|
|
gt := unwrapPointer(goType)
|
|
gk := gt.Kind()
|
|
if gk == reflect.Struct {
|
|
if f.Kind() != protoreflect.MessageKind {
|
|
t.Errorf("%s.%s.%s: discriminator %q expected message-typed oneof case but proto field is a scalar of kind %s", o.MessageName, o.OneofName, caseName, d, f.Kind())
|
|
continue
|
|
}
|
|
want := gt.Name()
|
|
got := string(f.Message().Name())
|
|
if got != want {
|
|
t.Errorf("%s.%s.%s: discriminator %q references Go type %s but proto case wraps message %s", o.MessageName, o.OneofName, caseName, d, want, got)
|
|
}
|
|
continue
|
|
}
|
|
// Scalar Go kind — validate proto kind compatibility.
|
|
if err := checkScalarKind(gt, f); err != nil {
|
|
t.Errorf("%s.%s.%s: discriminator %q: %v", o.MessageName, o.OneofName, caseName, d, err)
|
|
}
|
|
}
|
|
|
|
// Sort orphan-case names for deterministic error ordering.
|
|
var orphanCases []string
|
|
for caseName := range caseFields {
|
|
if !expectedCases[caseName] {
|
|
orphanCases = append(orphanCases, caseName)
|
|
}
|
|
}
|
|
sort.Strings(orphanCases)
|
|
for _, caseName := range orphanCases {
|
|
f := caseFields[caseName]
|
|
t.Errorf("%s.%s: proto case %q (number %d) has no corresponding discriminator in DiscriminatorToCase; either remove it (and `reserved %d` the number) or extend the spec", o.MessageName, o.OneofName, caseName, f.Number(), f.Number())
|
|
}
|
|
}
|
|
|
|
// jsonFieldName returns the JSON name of f and whether it should be included
|
|
// (false for `json:"-"` fields).
|
|
func jsonFieldName(f reflect.StructField) (string, bool) {
|
|
tag := f.Tag.Get("json")
|
|
if tag == "-" {
|
|
return "", false
|
|
}
|
|
if tag == "" {
|
|
return f.Name, true
|
|
}
|
|
name, _, _ := strings.Cut(tag, ",")
|
|
if name == "" {
|
|
name = f.Name
|
|
}
|
|
return name, true
|
|
}
|
|
|
|
// checkFieldType verifies that the Go field type goType is compatible with
|
|
// the proto field descriptor pf.
|
|
func checkFieldType(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
|
goType = unwrapPointer(goType)
|
|
|
|
if pf.IsList() {
|
|
if goType.Kind() != reflect.Slice && goType.Kind() != reflect.Array {
|
|
return fmt.Errorf("proto field is repeated but Go type is %s", goType.Kind())
|
|
}
|
|
elem := unwrapPointer(goType.Elem())
|
|
return checkScalarOrMessage(elem, pf)
|
|
}
|
|
if pf.IsMap() {
|
|
if goType.Kind() != reflect.Map {
|
|
return fmt.Errorf("proto field is a map but Go type is %s", goType.Kind())
|
|
}
|
|
if goType.Key().Kind() != reflect.String {
|
|
return fmt.Errorf("proto field is a map but Go map key is %s, want string", goType.Key().Kind())
|
|
}
|
|
valField := pf.MapValue()
|
|
valType := unwrapPointer(goType.Elem())
|
|
return checkScalarOrMessage(valType, valField)
|
|
}
|
|
return checkScalarOrMessage(goType, pf)
|
|
}
|
|
|
|
func checkScalarOrMessage(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
|
goType = unwrapPointer(goType)
|
|
if pf.Kind() == protoreflect.MessageKind || pf.Kind() == protoreflect.GroupKind {
|
|
// Accept any Go type for message-typed proto fields. The outer
|
|
// message check verifies field-by-field shape; the
|
|
// Stmt/Val/Operand/Block plumbing relies on this looseness.
|
|
return nil
|
|
}
|
|
return checkScalarKind(goType, pf)
|
|
}
|
|
|
|
// checkScalarKind verifies a Go type is compatible with a scalar proto kind.
|
|
// For integer kinds we enforce that the Go bit-width fits in the proto
|
|
// bit-width (so Go int64 cannot be silently mapped to proto int32). The
|
|
// platform-dependent `int`/`uint` kinds are treated as 32-bit minimum and
|
|
// accepted against either width — practical for OPA's bounded indices.
|
|
func checkScalarKind(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
|
switch pf.Kind() {
|
|
case protoreflect.BoolKind:
|
|
if goType.Kind() != reflect.Bool {
|
|
return fmt.Errorf("proto Kind=bool but Go type is %s", goType.Kind())
|
|
}
|
|
case protoreflect.StringKind:
|
|
if goType.Kind() != reflect.String {
|
|
return fmt.Errorf("proto Kind=string but Go type is %s", goType.Kind())
|
|
}
|
|
case protoreflect.BytesKind:
|
|
if !(goType.Kind() == reflect.Slice && goType.Elem().Kind() == reflect.Uint8) {
|
|
return fmt.Errorf("proto Kind=bytes but Go type is %s", goType.Kind())
|
|
}
|
|
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
|
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
|
if !signedIntFits(goType.Kind(), pf.Kind()) {
|
|
return fmt.Errorf("proto Kind=%s but Go type is %s (Go int64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
|
|
}
|
|
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
|
|
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
|
if !unsignedIntFits(goType.Kind(), pf.Kind()) {
|
|
return fmt.Errorf("proto Kind=%s but Go type is %s (Go uint64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
|
|
}
|
|
case protoreflect.FloatKind, protoreflect.DoubleKind:
|
|
if goType.Kind() != reflect.Float32 && goType.Kind() != reflect.Float64 {
|
|
return fmt.Errorf("proto Kind=%s but Go type is %s", pf.Kind(), goType.Kind())
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported proto Kind=%s", pf.Kind())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// signedIntFits reports whether a Go signed integer kind fits in a proto
|
|
// signed integer kind. Go's platform-dependent `int` is treated as
|
|
// at-most 32-bit (lenient — accepted against either int32 or int64 proto
|
|
// kinds). Fixed-width kinds (int8/int16/int32/int64) are checked strictly.
|
|
func signedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
|
|
proto64 := pfKind == protoreflect.Int64Kind || pfKind == protoreflect.Sint64Kind || pfKind == protoreflect.Sfixed64Kind
|
|
switch goKind {
|
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:
|
|
return true // fits any signed proto int kind
|
|
case reflect.Int64:
|
|
return proto64
|
|
}
|
|
return false
|
|
}
|
|
|
|
// unsignedIntFits is the unsigned counterpart of signedIntFits.
|
|
func unsignedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
|
|
proto64 := pfKind == protoreflect.Uint64Kind || pfKind == protoreflect.Fixed64Kind
|
|
switch goKind {
|
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint:
|
|
return true
|
|
case reflect.Uint64:
|
|
return proto64
|
|
}
|
|
return false
|
|
}
|
|
|
|
func unwrapPointer(t reflect.Type) reflect.Type {
|
|
for t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
return t
|
|
}
|