| // Copyright 2021-2023 Buf Technologies, Inc. |
| // |
| // 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 triple_protocol |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| ) |
| |
| import ( |
| "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 |
| } |
| |
| // 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) Unmarshal(data []byte, message any) error { |
| protoMessage, ok := message.(proto.Message) |
| if !ok { |
| return errNotProto(message) |
| } |
| return proto.Unmarshal(data, protoMessage) |
| } |
| |
| 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) |
| } |
| var options protojson.MarshalOptions |
| return options.Marshal(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") |
| } |
| var options protojson.UnmarshalOptions |
| return options.Unmarshal(binary, protoMessage) |
| } |
| |
| 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) |
| } |