istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/util/protomarshal/protomarshal.go (about)

     1  // Copyright Istio Authors
     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 protomarshal provides operations to marshal and unmarshal protobuf objects.
    16  // Unlike the rest of this repo, which uses the new google.golang.org/protobuf API, this package
    17  // explicitly uses the legacy jsonpb package. This is due to a number of compatibility concerns with the new API:
    18  // * https://github.com/golang/protobuf/issues/1374
    19  // * https://github.com/golang/protobuf/issues/1373
    20  package protomarshal
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"errors"
    26  	"strings"
    27  
    28  	"github.com/golang/protobuf/jsonpb"             // nolint: depguard
    29  	legacyproto "github.com/golang/protobuf/proto"  // nolint: staticcheck
    30  	"google.golang.org/protobuf/encoding/protojson" // nolint: depguard
    31  	"google.golang.org/protobuf/proto"
    32  	"google.golang.org/protobuf/reflect/protoreflect"
    33  	"google.golang.org/protobuf/reflect/protoregistry"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	"istio.io/istio/pkg/log"
    37  )
    38  
    39  var (
    40  	unmarshaler       = jsonpb.Unmarshaler{AllowUnknownFields: true}
    41  	strictUnmarshaler = jsonpb.Unmarshaler{}
    42  )
    43  
    44  func Unmarshal(b []byte, m proto.Message) error {
    45  	return strictUnmarshaler.Unmarshal(bytes.NewReader(b), legacyproto.MessageV1(m))
    46  }
    47  
    48  func UnmarshalString(s string, m proto.Message) error {
    49  	return Unmarshal([]byte(s), m)
    50  }
    51  
    52  func UnmarshalAllowUnknown(b []byte, m proto.Message) error {
    53  	return unmarshaler.Unmarshal(bytes.NewReader(b), legacyproto.MessageV1(m))
    54  }
    55  
    56  type resolver interface {
    57  	protoregistry.MessageTypeResolver
    58  	protoregistry.ExtensionTypeResolver
    59  }
    60  
    61  func UnmarshalAllowUnknownWithAnyResolver(anyResolver resolver, b []byte, m proto.Message) error {
    62  	return (&protojson.UnmarshalOptions{
    63  		DiscardUnknown: true,
    64  		Resolver:       anyResolver,
    65  	}).Unmarshal(b, m)
    66  }
    67  
    68  // ToJSON marshals a proto to canonical JSON
    69  func ToJSON(msg proto.Message) (string, error) {
    70  	return ToJSONWithIndent(msg, "")
    71  }
    72  
    73  // Marshal marshals a proto to canonical JSON
    74  func Marshal(msg proto.Message) ([]byte, error) {
    75  	res, err := ToJSONWithIndent(msg, "")
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	return []byte(res), err
    80  }
    81  
    82  // MarshalIndent marshals a proto to canonical JSON with indentation
    83  func MarshalIndent(msg proto.Message, indent string) ([]byte, error) {
    84  	res, err := ToJSONWithIndent(msg, indent)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	return []byte(res), err
    89  }
    90  
    91  // MarshalIndentWithGlobalTypesResolver marshals a proto to canonical JSON with indentation
    92  // and multiline while using generic types resolver
    93  func MarshalIndentWithGlobalTypesResolver(msg proto.Message, indent string) ([]byte, error) {
    94  	return protojson.MarshalOptions{
    95  		Multiline: true,
    96  		Indent:    indent,
    97  	}.Marshal(msg)
    98  }
    99  
   100  // MarshalProtoNames marshals a proto to canonical JSON original protobuf names
   101  func MarshalProtoNames(msg proto.Message) ([]byte, error) {
   102  	if msg == nil {
   103  		return nil, errors.New("unexpected nil message")
   104  	}
   105  
   106  	// Marshal from proto to json bytes
   107  	m := jsonpb.Marshaler{OrigName: true}
   108  	buf := &bytes.Buffer{}
   109  	err := m.Marshal(buf, legacyproto.MessageV1(msg))
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return buf.Bytes(), nil
   114  }
   115  
   116  // ToJSONWithIndent marshals a proto to canonical JSON with pretty printed string
   117  func ToJSONWithIndent(msg proto.Message, indent string) (string, error) {
   118  	return ToJSONWithOptions(msg, indent, false)
   119  }
   120  
   121  // ToJSONWithOptions marshals a proto to canonical JSON with options to indent and
   122  // print enums' int values
   123  func ToJSONWithOptions(msg proto.Message, indent string, enumsAsInts bool) (string, error) {
   124  	if msg == nil {
   125  		return "", errors.New("unexpected nil message")
   126  	}
   127  
   128  	// Marshal from proto to json bytes
   129  	m := jsonpb.Marshaler{Indent: indent, EnumsAsInts: enumsAsInts}
   130  	return m.MarshalToString(legacyproto.MessageV1(msg))
   131  }
   132  
   133  func ToJSONWithAnyResolver(msg proto.Message, indent string, anyResolver jsonpb.AnyResolver) (string, error) {
   134  	if msg == nil {
   135  		return "", errors.New("unexpected nil message")
   136  	}
   137  
   138  	m := jsonpb.Marshaler{
   139  		Indent:      indent,
   140  		AnyResolver: anyResolver,
   141  	}
   142  	return m.MarshalToString(legacyproto.MessageV1(msg))
   143  }
   144  
   145  // ToYAML marshals a proto to canonical YAML
   146  func ToYAML(msg proto.Message) (string, error) {
   147  	js, err := ToJSON(msg)
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  	yml, err := yaml.JSONToYAML([]byte(js))
   152  	return string(yml), err
   153  }
   154  
   155  // ToJSONMap converts a proto message to a generic map using canonical JSON encoding
   156  // JSON encoding is specified here: https://developers.google.com/protocol-buffers/docs/proto3#json
   157  func ToJSONMap(msg proto.Message) (map[string]any, error) {
   158  	js, err := ToJSON(msg)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// Unmarshal from json bytes to go map
   164  	var data map[string]any
   165  	err = json.Unmarshal([]byte(js), &data)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	return data, nil
   171  }
   172  
   173  // ApplyJSON unmarshals a JSON string into a proto message.
   174  func ApplyJSON(js string, pb proto.Message) error {
   175  	reader := strings.NewReader(js)
   176  	m := jsonpb.Unmarshaler{}
   177  	if err := m.Unmarshal(reader, legacyproto.MessageV1(pb)); err != nil {
   178  		log.Debugf("Failed to decode proto: %q. Trying decode with AllowUnknownFields=true", err)
   179  		m.AllowUnknownFields = true
   180  		reader.Reset(js)
   181  		return m.Unmarshal(reader, legacyproto.MessageV1(pb))
   182  	}
   183  	return nil
   184  }
   185  
   186  // ApplyJSONStrict unmarshals a JSON string into a proto message.
   187  func ApplyJSONStrict(js string, pb proto.Message) error {
   188  	reader := strings.NewReader(js)
   189  	m := jsonpb.Unmarshaler{}
   190  	return m.Unmarshal(reader, legacyproto.MessageV1(pb))
   191  }
   192  
   193  // ApplyYAML unmarshals a YAML string into a proto message.
   194  // Unknown fields are allowed.
   195  func ApplyYAML(yml string, pb proto.Message) error {
   196  	js, err := yaml.YAMLToJSON([]byte(yml))
   197  	if err != nil {
   198  		return err
   199  	}
   200  	return ApplyJSON(string(js), pb)
   201  }
   202  
   203  // ApplyYAMLStrict unmarshals a YAML string into a proto message.
   204  // Unknown fields are not allowed.
   205  func ApplyYAMLStrict(yml string, pb proto.Message) error {
   206  	js, err := yaml.YAMLToJSON([]byte(yml))
   207  	if err != nil {
   208  		return err
   209  	}
   210  	return ApplyJSONStrict(string(js), pb)
   211  }
   212  
   213  func ShallowCopy(dst, src proto.Message) {
   214  	dm := dst.ProtoReflect()
   215  	sm := src.ProtoReflect()
   216  	if dm.Type() != sm.Type() {
   217  		panic("mismatching type")
   218  	}
   219  	proto.Reset(dst)
   220  	sm.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
   221  		dm.Set(fd, v)
   222  		return true
   223  	})
   224  }