Skip to content

Commit

Permalink
Merge pull request #17 from stackb/hash-wkt-1
Browse files Browse the repository at this point in the history
Add partial support for well-known types
  • Loading branch information
pcj committed Jun 22, 2023
2 parents f0db6c0 + c7f2663 commit 18e068e
Show file tree
Hide file tree
Showing 22 changed files with 1,310 additions and 22 deletions.
4 changes: 4 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ load("@rules_go//go:def.bzl", "go_library", "go_test")
load("@gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/stackb/protoreflecthash
# gazelle:resolve go go github.com/stackb/protoreflecthash/test_protos/generated/latest/proto2 @com_github_stackb_protoreflecthash//test_protos/generated/latest/proto2
# gazelle:resolve go go github.com/stackb/protoreflecthash/test_protos/generated/latest/proto3 @com_github_stackb_protoreflecthash//test_protos/generated/latest/proto3

gazelle(name = "gazelle")

Expand Down Expand Up @@ -30,6 +32,7 @@ go_test(
embed = [":protoreflecthash"],
embedsrcs = ["testdata/protoset.pb"],
deps = [
"//test_protos/generated/latest/proto2",
"//test_protos/generated/latest/proto3",
"@com_github_benlaurie_objecthash//go/objecthash:go_default_library",
"@com_github_google_go_cmp//cmp",
Expand All @@ -40,5 +43,6 @@ go_test(
"@org_golang_google_protobuf//reflect/protoregistry",
"@org_golang_google_protobuf//types/descriptorpb",
"@org_golang_google_protobuf//types/dynamicpb",
"@org_golang_google_protobuf//types/known/timestamppb",
],
)
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ test_protoset:

# vendor in the generated files for regular go build process
pb_go:
bazel build //test_protos/schema/proto3:proto3_go_proto build //test_protos/schema/proto2:proto2_go_proto
bazel build //test_protos/schema/proto3:proto3_go_proto //test_protos/schema/proto2:proto2_go_proto
cp -f bazel-bin/test_protos/schema/proto3/proto3_go_proto_/github.com/stackb/protoreflecthash/test_protos/generated/latest/proto3/*.pb.go test_protos/generated/latest/proto3
cp -f bazel-bin/test_protos/schema/proto2/proto2_go_proto_/github.com/stackb/protoreflecthash/test_protos/generated/latest/proto2/*.pb.go test_protos/generated/latest/proto2
98 changes: 98 additions & 0 deletions hasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
)

const valueName = protoreflect.Name("value")

type ProtoHasherOption func(*hasher)

type ProtoHasher interface {
Expand Down Expand Up @@ -61,6 +63,10 @@ func (h *hasher) hashMessage(msg protoreflect.Message) ([]byte, error) {

md := msg.Descriptor()

if hash, err, ok := h.hashWellKnownType(md, msg); ok {
return hash, err
}

// TOOD(pcj): what is the correct handling of placeholder types?
if md.IsPlaceholder() {
return nil, nil
Expand Down Expand Up @@ -279,6 +285,98 @@ func (h *hasher) hashMap(kd, fd protoreflect.FieldDescriptor, m protoreflect.Map
return hash(mapIdentifier, buf.Bytes())
}

func (h *hasher) hashWellKnownType(md protoreflect.MessageDescriptor, msg protoreflect.Message) (hash []byte, err error, ok bool) {
switch md.FullName() {
case protoreflect.FullName("google.protobuf.Any"):
hash, err = h.hashGoogleProtobufAny(md, msg)
case protoreflect.FullName("google.protobuf.Duration"):
hash, err = h.hashGoogleProtobufDuration(md, msg)
case protoreflect.FullName("google.protobuf.Timestamp"):
hash, err = h.hashGoogleProtobufTimestamp(md, msg)
case protoreflect.FullName("google.protobuf.DoubleValue"):
hash, err = h.hashGoogleProtobufDoubleValue(md, msg)
case protoreflect.FullName("google.protobuf.FloatValue"):
hash, err = h.hashGoogleProtobufFloatValue(md, msg)
case protoreflect.FullName("google.protobuf.Int64Value"):
hash, err = h.hashGoogleProtobufInt64Value(md, msg)
case protoreflect.FullName("google.protobuf.Uint64Value"):
hash, err = h.hashGoogleProtobufUint64Value(md, msg)
case protoreflect.FullName("google.protobuf.Int32Value"):
hash, err = h.hashGoogleProtobufInt32Value(md, msg)
case protoreflect.FullName("google.protobuf.Uint32Value"):
hash, err = h.hashGoogleProtobufUint32Value(md, msg)
case protoreflect.FullName("google.protobuf.BoolValue"):
hash, err = h.hashGoogleProtobufBoolValue(md, msg)
case protoreflect.FullName("google.protobuf.StringValue"):
hash, err = h.hashGoogleProtobufStringValue(md, msg)
default:
return nil, nil, false // no special handling needed, use hashMessage
}
return hash, err, true
}

func (h *hasher) hashGoogleProtobufAny(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
// files := protoregistry.GlobalFiles - create option to set files explictly?
typeUrl := msg.Get(md.Fields().ByName("type_url")).String()
// TODO: lookup at a type server?
return nil, fmt.Errorf("unsupported type: " + typeUrl)
}

func (h *hasher) hashGoogleProtobufDuration(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashFieldsByName(md, msg, "seconds", "nanos")
}

func (h *hasher) hashGoogleProtobufTimestamp(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashFieldsByName(md, msg, "seconds", "nanos")
}

func (h *hasher) hashFieldsByName(md protoreflect.MessageDescriptor, msg protoreflect.Message, names ...string) ([]byte, error) {
var buf bytes.Buffer

for _, name := range names {
value := msg.Get(md.Fields().ByName(protoreflect.Name(name)))
data, err := h.hashValue(protoreflect.Int32Kind, value)
if err != nil {
return nil, fmt.Errorf("hashing %s: %w", md.FullName(), err)
}
buf.Write(data)
}

return hash(listIdentifier, buf.Bytes())
}

func (h *hasher) hashGoogleProtobufDoubleValue(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashFloat(msg.Get(md.Fields().ByName(valueName)).Float())
}

func (h *hasher) hashGoogleProtobufFloatValue(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashFloat(msg.Get(md.Fields().ByName(valueName)).Float())
}

func (h *hasher) hashGoogleProtobufInt64Value(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashInt(msg.Get(md.Fields().ByName(valueName)).Int())
}

func (h *hasher) hashGoogleProtobufUint64Value(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashUint(msg.Get(md.Fields().ByName(valueName)).Uint())
}

func (h *hasher) hashGoogleProtobufInt32Value(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashInt(msg.Get(md.Fields().ByName(valueName)).Int())
}

func (h *hasher) hashGoogleProtobufUint32Value(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashUint(msg.Get(md.Fields().ByName(valueName)).Uint())
}

func (h *hasher) hashGoogleProtobufBoolValue(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashBool(msg.Get(md.Fields().ByName(valueName)).Bool())
}

func (h *hasher) hashGoogleProtobufStringValue(md protoreflect.MessageDescriptor, msg protoreflect.Message) ([]byte, error) {
return h.hashString(msg.Get(md.Fields().ByName(valueName)).String())
}

type hashMapEntry struct {
khash []byte
vhash []byte
Expand Down
59 changes: 59 additions & 0 deletions hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"google.golang.org/protobuf/types/known/timestamppb"

pb2_latest "github.com/stackb/protoreflecthash/test_protos/generated/latest/proto2"
pb3_latest "github.com/stackb/protoreflecthash/test_protos/generated/latest/proto3"
Expand Down Expand Up @@ -981,6 +982,64 @@ func TestHashOtherTypes(t *testing.T) {
}
}

// TestHashOtherTypes performs tests on types that do not have their own test file.
func TestHashTimestamp(t *testing.T) {
for name, tc := range map[string]hashTestCase{
"Empty/Zero Timestamps": {
// The semantics of the Timestamp object imply that the distinction between
// unset and zero happen at the message level, rather than the field level.
//
// As a result, an unset timestamp is one where the proto itself is nil,
// while an explicitly set timestamp with unset fields is considered to be
// explicitly set to 0.
//
// This is unlike normal proto3 messages, where unset/zero fields must be
// considered to be unset, because they're indistinguishable in the general
// case.
protos: []proto.Message{
&timestamppb.Timestamp{},
&timestamppb.Timestamp{Seconds: 0, Nanos: 0},
},
// JSON treats all numbers as floats, so it is not possible to have an equivalent JSON string.
obj: []int64{0, 0},
want: "3a82b649344529f03f52c1833f5aecc488a53b31461a1f54c305d149b12b8f53",
},
"Normal Timestamps": {
protos: []proto.Message{
&timestamppb.Timestamp{Seconds: 1525450021, Nanos: 123456789},
},
// JSON treats all numbers as floats, so it is not possible to have an equivalent JSON string.
obj: []int64{1525450021, 123456789},
want: "1fd36770664df599ad44e4e4f06b1fad6ef7a4b3f316d79ca11bea668032a199",
},
"Timestamps within other protos (zero)": {
fieldNamesAsKeys: true,
protos: []proto.Message{
&pb2_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{}},
&pb2_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}},

&pb3_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{}},
&pb3_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}},
},
// JSON treats all numbers as floats, so it is not possible to have an equivalent JSON string.
obj: map[string][]int64{"timestamp_field": {0, 0}},
want: "8457fe431752dbc5c47301c2546fcf6f0ad8c5317092b443e187d18e312e497e",
},
"Timestamps within other protos (non-zero)": {
fieldNamesAsKeys: true,
protos: []proto.Message{
&pb2_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{Seconds: 1525450021, Nanos: 123456789}},
&pb3_latest.KnownTypes{TimestampField: &timestamppb.Timestamp{Seconds: 1525450021, Nanos: 123456789}},
},
// JSON treats all numbers as floats, so it is not possible to have an equivalent JSON string.
obj: map[string][]int64{"timestamp_field": {1525450021, 123456789}},
want: "cf99942e3f8d1212f4ce263e206d64e29525b97b91368e71f9595bce83ac6a3e",
},
} {
tc.Check(name, t)
}
}

func TestHashRepeatedFields(t *testing.T) {

for name, tc := range map[string]hashTestCase{
Expand Down
8 changes: 8 additions & 0 deletions test_protos/generated/latest/proto3/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "proto3",
srcs = [
"bad.pb.go",
"floats.pb.go",
"integers.pb.go",
"maps.pb.go",
"people.pb.go",
"planets.pb.go",
"simple.pb.go",
"well_known_types.pb.go",
],
importpath = "github.com/stackb/protoreflecthash/test_protos/generated/latest/proto3",
visibility = ["//visibility:public"],
deps = [
"@org_golang_google_protobuf//reflect/protoreflect",
"@org_golang_google_protobuf//runtime/protoimpl",
"@org_golang_google_protobuf//types/known/anypb",
"@org_golang_google_protobuf//types/known/durationpb",
"@org_golang_google_protobuf//types/known/structpb",
"@org_golang_google_protobuf//types/known/timestamppb",
"@org_golang_google_protobuf//types/known/wrapperspb",
],
)
67 changes: 67 additions & 0 deletions test_protos/generated/latest/proto3/bad.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions test_protos/generated/latest/proto3/floats.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion test_protos/generated/latest/proto3/integers.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions test_protos/generated/latest/proto3/maps.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 18e068e

Please sign in to comment.