// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package connect import ( "bytes" "encoding/json" "errors" "fmt" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoiface" ) const ( codecNameProto = "proto" codecNameJSON = "json" codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8" ) // Codec marshals structs (typically generated from a schema) to and from bytes. type Codec interface { // Name returns the name of the Codec. // // This may be used as part of the Content-Type within HTTP. For example, // with gRPC this is the content subtype, so "application/grpc+proto" will // map to the Codec with name "proto". // // Names must not be empty. Name() string // Marshal marshals the given message. // // Marshal may expect a specific type of message, and will error if this type // is not given. Marshal(any) ([]byte, error) // Unmarshal unmarshals the given message. // // Unmarshal may expect a specific type of message, and will error if this // type is not given. Unmarshal([]byte, any) error } // marshalAppender is an extension to Codec for appending to a byte slice. type marshalAppender interface { Codec // MarshalAppend marshals the given message and appends it to the given // byte slice. // // MarshalAppend may expect a specific type of message, and will error if // this type is not given. MarshalAppend([]byte, any) ([]byte, error) } // stableCodec is an extension to Codec for serializing with stable output. type stableCodec interface { Codec // MarshalStable marshals the given message with stable field ordering. // // MarshalStable should return the same output for a given input. Although // it is not guaranteed to be canonicalized, the marshalling routine for // MarshalStable will opt for the most normalized output available for a // given serialization. // // For practical reasons, it is possible for MarshalStable to return two // different results for two inputs considered to be "equal" in their own // domain, and it may change in the future with codec updates, but for // any given concrete value and any given version, it should return the // same output. MarshalStable(any) ([]byte, error) // IsBinary returns true if the marshalled data is binary for this codec. // // If this function returns false, the data returned from Marshal and // MarshalStable are considered valid text and may be used in contexts // where text is expected. IsBinary() bool } type protoBinaryCodec struct{} var _ Codec = (*protoBinaryCodec)(nil) func (c *protoBinaryCodec) Name() string { return codecNameProto } func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) { protoMessage, ok := message.(proto.Message) if !ok { return nil, errNotProto(message) } return proto.Marshal(protoMessage) } func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { protoMessage, ok := message.(proto.Message) if !ok { return nil, errNotProto(message) } return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage) } func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error { protoMessage, ok := message.(proto.Message) if !ok { return errNotProto(message) } err := proto.Unmarshal(data, protoMessage) if err != nil { return fmt.Errorf("unmarshal into %T: %w", message, err) } return nil } func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) { protoMessage, ok := message.(proto.Message) if !ok { return nil, errNotProto(message) } // protobuf does not offer a canonical output today, so this format is not // guaranteed to match deterministic output from other protobuf libraries. // In addition, unknown fields may cause inconsistent output for otherwise // equal messages. // https://github.com/golang/protobuf/issues/1121 options := proto.MarshalOptions{Deterministic: true} return options.Marshal(protoMessage) } func (c *protoBinaryCodec) IsBinary() bool { return true } type protoJSONCodec struct { name string } var _ Codec = (*protoJSONCodec)(nil) func (c *protoJSONCodec) Name() string { return c.name } func (c *protoJSONCodec) Marshal(message any) ([]byte, error) { protoMessage, ok := message.(proto.Message) if !ok { return nil, errNotProto(message) } return protojson.MarshalOptions{}.Marshal(protoMessage) } func (c *protoJSONCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { protoMessage, ok := message.(proto.Message) if !ok { return nil, errNotProto(message) } return protojson.MarshalOptions{}.MarshalAppend(dst, protoMessage) } func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error { protoMessage, ok := message.(proto.Message) if !ok { return errNotProto(message) } if len(binary) == 0 { return errors.New("zero-length payload is not a valid JSON object") } // Discard unknown fields so clients and servers aren't forced to always use // exactly the same version of the schema. options := protojson.UnmarshalOptions{DiscardUnknown: true} err := options.Unmarshal(binary, protoMessage) if err != nil { return fmt.Errorf("unmarshal into %T: %w", message, err) } return nil } func (c *protoJSONCodec) MarshalStable(message any) ([]byte, error) { // protojson does not offer a "deterministic" field ordering, but fields // are still ordered consistently by their index. However, protojson can // output inconsistent whitespace for some reason, therefore it is // suggested to use a formatter to ensure consistent formatting. // https://github.com/golang/protobuf/issues/1373 messageJSON, err := c.Marshal(message) if err != nil { return nil, err } compactedJSON := bytes.NewBuffer(messageJSON[:0]) if err = json.Compact(compactedJSON, messageJSON); err != nil { return nil, err } return compactedJSON.Bytes(), nil } func (c *protoJSONCodec) IsBinary() bool { return false } // readOnlyCodecs is a read-only interface to a map of named codecs. type readOnlyCodecs interface { // Get gets the Codec with the given name. Get(string) Codec // Protobuf gets the user-supplied protobuf codec, falling back to the default // implementation if necessary. // // This is helpful in the gRPC protocol, where the wire protocol requires // marshaling protobuf structs to binary even if the RPC procedures were // generated from a different IDL. Protobuf() Codec // Names returns a copy of the registered codec names. The returned slice is // safe for the caller to mutate. Names() []string } func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs { return &codecMap{ nameToCodec: nameToCodec, } } type codecMap struct { nameToCodec map[string]Codec } func (m *codecMap) Get(name string) Codec { return m.nameToCodec[name] } func (m *codecMap) Protobuf() Codec { if pb, ok := m.nameToCodec[codecNameProto]; ok { return pb } return &protoBinaryCodec{} } func (m *codecMap) Names() []string { names := make([]string, 0, len(m.nameToCodec)) for name := range m.nameToCodec { names = append(names, name) } return names } func errNotProto(message any) error { if _, ok := message.(protoiface.MessageV1); ok { return fmt.Errorf("%T uses github.com/golang/protobuf, but connect-go only supports google.golang.org/protobuf: see https://go.dev/blog/protobuf-apiv2", message) } return fmt.Errorf("%T doesn't implement proto.Message", message) }