diff --git a/internal/encoding/text/encode.go b/internal/encoding/text/encode.go index aa66bdd0..da289ccc 100644 --- a/internal/encoding/text/encode.go +++ b/internal/encoding/text/encode.go @@ -263,3 +263,8 @@ func (e *Encoder) Snapshot() encoderState { func (e *Encoder) Reset(es encoderState) { e.encoderState = es } + +// AppendString appends the escaped form of the input string to b. +func AppendString(b []byte, s string) []byte { + return appendString(b, s, false) +} diff --git a/internal/msgfmt/format.go b/internal/msgfmt/format.go index 6561b8b7..a319550f 100644 --- a/internal/msgfmt/format.go +++ b/internal/msgfmt/format.go @@ -30,8 +30,15 @@ func Format(m proto.Message) string { return string(appendMessage(nil, m.ProtoReflect())) } +// FormatValue returns a formatted string for an arbitrary value. +func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string { + return string(appendValue(nil, v, fd)) +} + func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor) []byte { switch v := v.Interface().(type) { + case nil: + return append(b, ""...) case bool, int32, int64, uint32, uint64, float32, float64: return append(b, fmt.Sprint(v)...) case string: @@ -39,7 +46,7 @@ func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor case []byte: return append(b, strconv.Quote(string(v))...) case protoreflect.EnumNumber: - return appendEnum(b, v, fd.Enum()) + return appendEnum(b, v, fd) case protoreflect.Message: return appendMessage(b, v) case protoreflect.List: @@ -51,9 +58,11 @@ func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor } } -func appendEnum(b []byte, v protoreflect.EnumNumber, ed protoreflect.EnumDescriptor) []byte { - if ev := ed.Values().ByNumber(v); ev != nil { - return append(b, ev.Name()...) +func appendEnum(b []byte, v protoreflect.EnumNumber, fd protoreflect.FieldDescriptor) []byte { + if fd != nil { + if ev := fd.Enum().Values().ByNumber(v); ev != nil { + return append(b, ev.Name()...) + } } return strconv.AppendInt(b, int64(v), 10) } diff --git a/internal/testprotos/news/news.pb.go b/internal/testprotos/news/news.pb.go new file mode 100644 index 00000000..bb728a69 --- /dev/null +++ b/internal/testprotos/news/news.pb.go @@ -0,0 +1,421 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: internal/testprotos/news/news.proto + +package news + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +type Article_Status int32 + +const ( + Article_DRAFT Article_Status = 0 + Article_PUBLISHED Article_Status = 1 + Article_REVOKED Article_Status = 2 +) + +// Enum value maps for Article_Status. +var ( + Article_Status_name = map[int32]string{ + 0: "DRAFT", + 1: "PUBLISHED", + 2: "REVOKED", + } + Article_Status_value = map[string]int32{ + "DRAFT": 0, + "PUBLISHED": 1, + "REVOKED": 2, + } +) + +func (x Article_Status) Enum() *Article_Status { + p := new(Article_Status) + *p = x + return p +} + +func (x Article_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Article_Status) Descriptor() protoreflect.EnumDescriptor { + return file_internal_testprotos_news_news_proto_enumTypes[0].Descriptor() +} + +func (Article_Status) Type() protoreflect.EnumType { + return &file_internal_testprotos_news_news_proto_enumTypes[0] +} + +func (x Article_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Article_Status.Descriptor instead. +func (Article_Status) EnumDescriptor() ([]byte, []int) { + return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0, 0} +} + +type Article struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Author string `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"` + Date *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=date,proto3" json:"date,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` + Status Article_Status `protobuf:"varint,8,opt,name=status,proto3,enum=google.golang.org.Article_Status" json:"status,omitempty"` + Tags []string `protobuf:"bytes,7,rep,name=tags,proto3" json:"tags,omitempty"` + Attachments []*anypb.Any `protobuf:"bytes,6,rep,name=attachments,proto3" json:"attachments,omitempty"` +} + +func (x *Article) Reset() { + *x = Article{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_testprotos_news_news_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Article) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Article) ProtoMessage() {} + +func (x *Article) ProtoReflect() protoreflect.Message { + mi := &file_internal_testprotos_news_news_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Article.ProtoReflect.Descriptor instead. +func (*Article) Descriptor() ([]byte, []int) { + return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0} +} + +func (x *Article) GetAuthor() string { + if x != nil { + return x.Author + } + return "" +} + +func (x *Article) GetDate() *timestamppb.Timestamp { + if x != nil { + return x.Date + } + return nil +} + +func (x *Article) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Article) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Article) GetStatus() Article_Status { + if x != nil { + return x.Status + } + return Article_DRAFT +} + +func (x *Article) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *Article) GetAttachments() []*anypb.Any { + if x != nil { + return x.Attachments + } + return nil +} + +type BinaryAttachment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *BinaryAttachment) Reset() { + *x = BinaryAttachment{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_testprotos_news_news_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BinaryAttachment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BinaryAttachment) ProtoMessage() {} + +func (x *BinaryAttachment) ProtoReflect() protoreflect.Message { + mi := &file_internal_testprotos_news_news_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BinaryAttachment.ProtoReflect.Descriptor instead. +func (*BinaryAttachment) Descriptor() ([]byte, []int) { + return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{1} +} + +func (x *BinaryAttachment) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *BinaryAttachment) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type KeyValueAttachment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Data map[string]string `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *KeyValueAttachment) Reset() { + *x = KeyValueAttachment{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_testprotos_news_news_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyValueAttachment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyValueAttachment) ProtoMessage() {} + +func (x *KeyValueAttachment) ProtoReflect() protoreflect.Message { + mi := &file_internal_testprotos_news_news_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyValueAttachment.ProtoReflect.Descriptor instead. +func (*KeyValueAttachment) Descriptor() ([]byte, []int) { + return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyValueAttachment) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *KeyValueAttachment) GetData() map[string]string { + if x != nil { + return x.Data + } + return nil +} + +var File_internal_testprotos_news_news_proto protoreflect.FileDescriptor + +var file_internal_testprotos_news_news_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, 0x6f, + 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb9, 0x02, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x41, 0x72, 0x74, + 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, + 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, + 0x6e, 0x79, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, + 0x2f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x52, 0x41, + 0x46, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02, + 0x22, 0x3a, 0x0a, 0x10, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa6, 0x01, 0x0a, + 0x12, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, + 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, + 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, + 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_internal_testprotos_news_news_proto_rawDescOnce sync.Once + file_internal_testprotos_news_news_proto_rawDescData = file_internal_testprotos_news_news_proto_rawDesc +) + +func file_internal_testprotos_news_news_proto_rawDescGZIP() []byte { + file_internal_testprotos_news_news_proto_rawDescOnce.Do(func() { + file_internal_testprotos_news_news_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_testprotos_news_news_proto_rawDescData) + }) + return file_internal_testprotos_news_news_proto_rawDescData +} + +var file_internal_testprotos_news_news_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_internal_testprotos_news_news_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_internal_testprotos_news_news_proto_goTypes = []interface{}{ + (Article_Status)(0), // 0: google.golang.org.Article.Status + (*Article)(nil), // 1: google.golang.org.Article + (*BinaryAttachment)(nil), // 2: google.golang.org.BinaryAttachment + (*KeyValueAttachment)(nil), // 3: google.golang.org.KeyValueAttachment + nil, // 4: google.golang.org.KeyValueAttachment.DataEntry + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*anypb.Any)(nil), // 6: google.protobuf.Any +} +var file_internal_testprotos_news_news_proto_depIdxs = []int32{ + 5, // 0: google.golang.org.Article.date:type_name -> google.protobuf.Timestamp + 0, // 1: google.golang.org.Article.status:type_name -> google.golang.org.Article.Status + 6, // 2: google.golang.org.Article.attachments:type_name -> google.protobuf.Any + 4, // 3: google.golang.org.KeyValueAttachment.data:type_name -> google.golang.org.KeyValueAttachment.DataEntry + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_internal_testprotos_news_news_proto_init() } +func file_internal_testprotos_news_news_proto_init() { + if File_internal_testprotos_news_news_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_internal_testprotos_news_news_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Article); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_testprotos_news_news_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BinaryAttachment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_testprotos_news_news_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyValueAttachment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_internal_testprotos_news_news_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_internal_testprotos_news_news_proto_goTypes, + DependencyIndexes: file_internal_testprotos_news_news_proto_depIdxs, + EnumInfos: file_internal_testprotos_news_news_proto_enumTypes, + MessageInfos: file_internal_testprotos_news_news_proto_msgTypes, + }.Build() + File_internal_testprotos_news_news_proto = out.File + file_internal_testprotos_news_news_proto_rawDesc = nil + file_internal_testprotos_news_news_proto_goTypes = nil + file_internal_testprotos_news_news_proto_depIdxs = nil +} diff --git a/internal/testprotos/news/news.proto b/internal/testprotos/news/news.proto new file mode 100644 index 00000000..bf56f3b8 --- /dev/null +++ b/internal/testprotos/news/news.proto @@ -0,0 +1,38 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +syntax = "proto3"; + +package google.golang.org; + +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "google.golang.org/protobuf/internal/testprotos/news"; + +message Article { + enum Status { + DRAFT = 0; + PUBLISHED = 1; + REVOKED = 2; + } + + string author = 1; + google.protobuf.Timestamp date = 2; + string title = 3; + string content = 4; + Status status = 8; + repeated string tags = 7; + repeated google.protobuf.Any attachments = 6; +} + +message BinaryAttachment { + string name = 1; + bytes data = 2; +} + +message KeyValueAttachment { + string name = 1; + map data = 2; +} \ No newline at end of file diff --git a/reflect/protopath/path.go b/reflect/protopath/path.go new file mode 100644 index 00000000..07f839d7 --- /dev/null +++ b/reflect/protopath/path.go @@ -0,0 +1,121 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package protopath provides functionality for +// representing a sequence of protobuf reflection operations on a message. +package protopath + +import ( + "fmt" + + "google.golang.org/protobuf/internal/msgfmt" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// NOTE: The Path and Values are separate types here since there are use cases +// where you would like to "address" some value in a message with just the path +// and don't have the value information available. +// +// This is different from how "github.com/google/go-cmp/cmp".Path operates, +// which combines both path and value information together. +// Since the cmp package itself is the only one ever constructing a cmp.Path, +// it will always have the value available. + +// Path is a sequence of protobuf reflection steps applied to some root +// protobuf message value to arrive at the current value. +// The first step must be a Root step. +type Path []Step + +// TODO: Provide a Parse function that parses something similar to or +// perhaps identical to the output of Path.String. + +// Index returns the ith step in the path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// It returns a zero Step value if the index is out-of-bounds. +func (p Path) Index(i int) Step { + if i < 0 { + i = len(p) + i + } + if i < 0 || i >= len(p) { + return Step{} + } + return p[i] +} + +// String returns a structured representation of the path +// by concatenating the string representation of every path step. +func (p Path) String() string { + var b []byte + for _, s := range p { + b = s.appendString(b) + } + return string(b) +} + +// Values is a Path paired with a sequence of values at each step. +// The lengths of Path and Values must be identical. +// The first step must be a Root step and +// the first value must be a concrete message value. +type Values struct { + Path Path + Values []protoreflect.Value +} + +// Len reports the length of the path and values. +// If the path and values have differing length, it returns the minimum length. +func (p Values) Len() int { + n := len(p.Path) + if n > len(p.Values) { + n = len(p.Values) + } + return n +} + +// Index returns the ith step and value and supports negative indexing. +// A negative index starts counting from the tail of the Values such that -1 +// refers to the last pair, -2 refers to the second-to-last pair, and so on. +func (p Values) Index(i int) (out struct { + Step Step + Value protoreflect.Value +}) { + // NOTE: This returns a single struct instead of two return values so that + // callers can make use of the the value in an expression: + // vs.Index(i).Value.Interface() + n := p.Len() + if i < 0 { + i = n + i + } + if i < 0 || i >= n { + return out + } + out.Step = p.Path[i] + out.Value = p.Values[i] + return out +} + +// String returns a humanly readable representation of the path and last value. +// Do not depend on the output being stable. +// +// For example: +// (path.to.MyMessage).list_field[5].map_field["hello"] = {hello: "world"} +func (p Values) String() string { + n := p.Len() + if n == 0 { + return "" + } + + // Determine the field descriptor associated with the last step. + var fd protoreflect.FieldDescriptor + last := p.Index(-1) + switch last.Step.kind { + case FieldAccessStep: + fd = last.Step.FieldDescriptor() + case MapIndexStep, ListIndexStep: + fd = p.Index(-2).Step.FieldDescriptor() + } + + // Format the full path with the last value. + return fmt.Sprintf("%v = %v", p.Path[:n], msgfmt.FormatValue(last.Value, fd)) +} diff --git a/reflect/protopath/step.go b/reflect/protopath/step.go new file mode 100644 index 00000000..95ae85c5 --- /dev/null +++ b/reflect/protopath/step.go @@ -0,0 +1,241 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protopath + +import ( + "fmt" + "strconv" + "strings" + + "google.golang.org/protobuf/internal/encoding/text" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// StepKind identifies the kind of step operation. +// Each kind of step corresponds with some protobuf reflection operation. +type StepKind int + +const ( + invalidStep StepKind = iota + // RootStep identifies a step as the Root step operation. + RootStep + // FieldAccessStep identifies a step as the FieldAccess step operation. + FieldAccessStep + // UnknownAccessStep identifies a step as the UnknownAccess step operation. + UnknownAccessStep + // ListIndexStep identifies a step as the ListIndex step operation. + ListIndexStep + // MapIndexStep identifies a step as the MapIndex step operation. + MapIndexStep + // AnyExpandStep identifies a step as the AnyExpand step operation. + AnyExpandStep +) + +func (k StepKind) String() string { + switch k { + case invalidStep: + return "" + case RootStep: + return "Root" + case FieldAccessStep: + return "FieldAccess" + case UnknownAccessStep: + return "UnknownAccess" + case ListIndexStep: + return "ListIndex" + case MapIndexStep: + return "MapIndex" + case AnyExpandStep: + return "AnyExpand" + default: + return fmt.Sprintf("", k) + } +} + +// Step is a union where only one step operation may be specified at a time. +// The different kinds of steps are specified by the constants defined for +// the StepKind type. +type Step struct { + kind StepKind + desc protoreflect.Descriptor + key protoreflect.Value +} + +// Root indicates the root message that a path is relative to. +// It should always (and only ever) be the first step in a path. +func Root(md protoreflect.MessageDescriptor) Step { + if md == nil { + panic("nil message descriptor") + } + return Step{kind: RootStep, desc: md} +} + +// FieldAccess describes access of a field within a message. +// Extension field accesses are also represented using a FieldAccess and +// must be provided with a protoreflect.FieldDescriptor +// +// Within the context of Values, +// the type of the previous step value is always a message, and +// the type of the current step value is determined by the field descriptor. +func FieldAccess(fd protoreflect.FieldDescriptor) Step { + if fd == nil { + panic("nil field descriptor") + } else if _, ok := fd.(protoreflect.ExtensionTypeDescriptor); !ok && fd.IsExtension() { + panic(fmt.Sprintf("extension field %q must implement protoreflect.ExtensionTypeDescriptor", fd.FullName())) + } + return Step{kind: FieldAccessStep, desc: fd} +} + +// UnknownAccess describes access to the unknown fields within a message. +// +// Within the context of Values, +// the type of the previous step value is always a message, and +// the type of the current step value is always a bytes type. +func UnknownAccess() Step { + return Step{kind: UnknownAccessStep} +} + +// ListIndex describes index of an element within a list. +// +// Within the context of Values, +// the type of the previous, previous step value is always a message, +// the type of the previous step value is always a list, and +// the type of the current step value is determined by the field descriptor. +func ListIndex(i int) Step { + if i < 0 { + panic(fmt.Sprintf("invalid list index: %v", i)) + } + return Step{kind: ListIndexStep, key: protoreflect.ValueOfInt64(int64(i))} +} + +// MapIndex describes index of an entry within a map. +// The key type is determined by field descriptor that the map belongs to. +// +// Within the context of Values, +// the type of the previous previous step value is always a message, +// the type of the previous step value is always a map, and +// the type of the current step value is determined by the field descriptor. +func MapIndex(k protoreflect.MapKey) Step { + if !k.IsValid() { + panic("invalid map index") + } + return Step{kind: MapIndexStep, key: k.Value()} +} + +// AnyExpand describes expansion of a google.protobuf.Any message into +// a structured representation of the underlying message. +// +// Within the context of Values, +// the type of the previous step value is always a google.protobuf.Any message, and +// the type of the current step value is always a message. +func AnyExpand(md protoreflect.MessageDescriptor) Step { + if md == nil { + panic("nil message descriptor") + } + return Step{kind: AnyExpandStep, desc: md} +} + +// MessageDescriptor returns the message descriptor for Root or AnyExpand steps, +// otherwise it returns nil. +func (s Step) MessageDescriptor() protoreflect.MessageDescriptor { + switch s.kind { + case RootStep, AnyExpandStep: + return s.desc.(protoreflect.MessageDescriptor) + default: + return nil + } +} + +// FieldDescriptor returns the field descriptor for FieldAccess steps, +// otherwise it returns nil. +func (s Step) FieldDescriptor() protoreflect.FieldDescriptor { + switch s.kind { + case FieldAccessStep: + return s.desc.(protoreflect.FieldDescriptor) + default: + return nil + } +} + +// ListIndex returns the list index for ListIndex steps, +// otherwise it returns 0. +func (s Step) ListIndex() int { + switch s.kind { + case ListIndexStep: + return int(s.key.Int()) + default: + return 0 + } +} + +// MapIndex returns the map key for MapIndex steps, +// otherwise it returns an invalid map key. +func (s Step) MapIndex() protoreflect.MapKey { + switch s.kind { + case MapIndexStep: + return s.key.MapKey() + default: + return protoreflect.MapKey{} + } +} + +// Kind reports which kind of step this is. +func (s Step) Kind() StepKind { + return s.kind +} + +func (s Step) String() string { + return string(s.appendString(nil)) +} + +func (s Step) appendString(b []byte) []byte { + switch s.kind { + case RootStep: + b = append(b, '(') + b = append(b, s.desc.FullName()...) + b = append(b, ')') + case FieldAccessStep: + b = append(b, '.') + if fd := s.desc.(protoreflect.FieldDescriptor); fd.IsExtension() { + b = append(b, '(') + b = append(b, strings.Trim(fd.TextName(), "[]")...) + b = append(b, ')') + } else { + b = append(b, fd.TextName()...) + } + case UnknownAccessStep: + b = append(b, '.') + b = append(b, '?') + case ListIndexStep: + b = append(b, '[') + b = strconv.AppendInt(b, s.key.Int(), 10) + b = append(b, ']') + case MapIndexStep: + b = append(b, '[') + switch k := s.key.Interface().(type) { + case bool: + b = strconv.AppendBool(b, bool(k)) // e.g., "true" or "false" + case int32: + b = strconv.AppendInt(b, int64(k), 10) // e.g., "-32" + case int64: + b = strconv.AppendInt(b, int64(k), 10) // e.g., "-64" + case uint32: + b = strconv.AppendUint(b, uint64(k), 10) // e.g., "32" + case uint64: + b = strconv.AppendUint(b, uint64(k), 10) // e.g., "64" + case string: + b = text.AppendString(b, k) // e.g., `"hello, world"` + } + b = append(b, ']') + case AnyExpandStep: + b = append(b, '.') + b = append(b, '(') + b = append(b, s.desc.FullName()...) + b = append(b, ')') + default: + b = append(b, ""...) + } + return b +} diff --git a/reflect/protorange/example_test.go b/reflect/protorange/example_test.go new file mode 100644 index 00000000..90ceec6c --- /dev/null +++ b/reflect/protorange/example_test.go @@ -0,0 +1,307 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protorange_test + +import ( + "fmt" + "strings" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/internal/detrand" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protopath" + "google.golang.org/protobuf/reflect/protorange" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/testing/protopack" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/timestamppb" + + newspb "google.golang.org/protobuf/internal/testprotos/news" +) + +func init() { + detrand.Disable() +} + +func mustMarshal(m proto.Message) []byte { + b, err := proto.Marshal(m) + if err != nil { + panic(err) + } + return b +} + +// Range through every message and clear the unknown fields. +func Example_discardUnknown() { + // Populate the article with unknown fields. + m := &newspb.Article{} + m.ProtoReflect().SetUnknown(protopack.Message{ + protopack.Tag{1000, protopack.BytesType}, protopack.String("Hello, world!"), + }.Marshal()) + fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0) + + // Range through the message and clear all unknown fields. + fmt.Println("clear unknown fields") + protorange.Range(m.ProtoReflect(), func(proto protopath.Values) error { + m, ok := proto.Index(-1).Value.Interface().(protoreflect.Message) + if ok && len(m.GetUnknown()) > 0 { + m.SetUnknown(nil) + } + return nil + }) + fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0) + + // Output: + // has unknown fields? true + // clear unknown fields + // has unknown fields? false +} + +// Print the relative paths as Range iterates through a message +// in a depth-first order. +func Example_printPaths() { + m := &newspb.Article{ + Author: "Russ Cox", + Date: timestamppb.New(time.Date(2019, time.November, 8, 0, 0, 0, 0, time.UTC)), + Title: "Go Turns 10", + Content: "Happy birthday, Go! This weekend we celebrate the 10th anniversary of the Go release...", + Status: newspb.Article_PUBLISHED, + Tags: []string{"community", "birthday"}, + Attachments: []*anypb.Any{{ + TypeUrl: "google.golang.org.BinaryAttachment", + Value: mustMarshal(&newspb.BinaryAttachment{ + Name: "gopher-birthday.png", + Data: []byte(""), + }), + }}, + } + + // Traverse over all reachable values and print the path. + protorange.Range(m.ProtoReflect(), func(p protopath.Values) error { + fmt.Println(p.Path[1:]) + return nil + }) + + // Output: + // .author + // .date + // .date.seconds + // .title + // .content + // .status + // .tags + // .tags[0] + // .tags[1] + // .attachments + // .attachments[0] + // .attachments[0].(google.golang.org.BinaryAttachment) + // .attachments[0].(google.golang.org.BinaryAttachment).name + // .attachments[0].(google.golang.org.BinaryAttachment).data +} + +// Implement a basic text formatter by ranging through all populated values +// in a message in depth-first order. +func Example_formatText() { + m := &newspb.Article{ + Author: "Brad Fitzpatrick", + Date: timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)), + Title: "Go 1.10 is released", + Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...", + Status: newspb.Article_PUBLISHED, + Tags: []string{"go1.10", "release"}, + Attachments: []*anypb.Any{{ + TypeUrl: "google.golang.org.KeyValueAttachment", + Value: mustMarshal(&newspb.KeyValueAttachment{ + Name: "checksums.txt", + Data: map[string]string{ + "go1.10.src.tar.gz": "07cbb9d0091b846c6aea40bf5bc0cea7", + "go1.10.darwin-amd64.pkg": "cbb38bb6ff6ea86279e01745984445bf", + "go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566", + "go1.10.windows-amd64.msi": "57bda02030f58f5d2bf71943e1390123", + }, + }), + }}, + } + + // Print a message in a humanly readable format. + var indent []byte + protorange.Options{ + Stable: true, + }.Range(m.ProtoReflect(), + func(p protopath.Values) error { + // Print the key. + var fd protoreflect.FieldDescriptor + last := p.Index(-1) + beforeLast := p.Index(-2) + switch last.Step.Kind() { + case protopath.FieldAccessStep: + fd = last.Step.FieldDescriptor() + fmt.Printf("%s%s: ", indent, fd.Name()) + case protopath.ListIndexStep: + fd = beforeLast.Step.FieldDescriptor() // lists always appear in the context of a repeated field + fmt.Printf("%s%d: ", indent, last.Step.ListIndex()) + case protopath.MapIndexStep: + fd = beforeLast.Step.FieldDescriptor() // maps always appear in the context of a repeated field + fmt.Printf("%s%v: ", indent, last.Step.MapIndex().Interface()) + case protopath.AnyExpandStep: + fmt.Printf("%s[%v]: ", indent, last.Value.Message().Descriptor().FullName()) + case protopath.UnknownAccessStep: + fmt.Printf("%s?: ", indent) + } + + // Starting printing the value. + switch v := last.Value.Interface().(type) { + case protoreflect.Message: + fmt.Printf("{\n") + indent = append(indent, '\t') + case protoreflect.List: + fmt.Printf("[\n") + indent = append(indent, '\t') + case protoreflect.Map: + fmt.Printf("{\n") + indent = append(indent, '\t') + case protoreflect.EnumNumber: + var ev protoreflect.EnumValueDescriptor + if fd != nil { + ev = fd.Enum().Values().ByNumber(v) + } + if ev != nil { + fmt.Printf("%v\n", ev.Name()) + } else { + fmt.Printf("%v\n", v) + } + case string, []byte: + fmt.Printf("%q\n", v) + default: + fmt.Printf("%v\n", v) + } + return nil + }, + func(p protopath.Values) error { + // Finish printing the value. + last := p.Index(-1) + switch last.Value.Interface().(type) { + case protoreflect.Message: + indent = indent[:len(indent)-1] + fmt.Printf("%s}\n", indent) + case protoreflect.List: + indent = indent[:len(indent)-1] + fmt.Printf("%s]\n", indent) + case protoreflect.Map: + indent = indent[:len(indent)-1] + fmt.Printf("%s}\n", indent) + } + return nil + }, + ) + + // Output: + // { + // author: "Brad Fitzpatrick" + // date: { + // seconds: 1518739200 + // } + // title: "Go 1.10 is released" + // content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10..." + // attachments: [ + // 0: { + // [google.golang.org.KeyValueAttachment]: { + // name: "checksums.txt" + // data: { + // go1.10.darwin-amd64.pkg: "cbb38bb6ff6ea86279e01745984445bf" + // go1.10.linux-amd64.tar.gz: "6b3d0e4a5c77352cf4275573817f7566" + // go1.10.src.tar.gz: "07cbb9d0091b846c6aea40bf5bc0cea7" + // go1.10.windows-amd64.msi: "57bda02030f58f5d2bf71943e1390123" + // } + // } + // } + // ] + // tags: [ + // 0: "go1.10" + // 1: "release" + // ] + // status: PUBLISHED + // } +} + +// Scan all protobuf string values for a sensitive word and replace it with +// a suitable alternative. +func Example_sanitizeStrings() { + m := &newspb.Article{ + Author: "Hermione Granger", + Date: timestamppb.New(time.Date(1998, time.May, 2, 0, 0, 0, 0, time.UTC)), + Title: "Harry Potter vanquishes Voldemort once and for all!", + Content: "In a final duel between Harry Potter and Lord Voldemort earlier this evening...", + Tags: []string{"HarryPotter", "LordVoldemort"}, + Attachments: []*anypb.Any{{ + TypeUrl: "google.golang.org.KeyValueAttachment", + Value: mustMarshal(&newspb.KeyValueAttachment{ + Name: "aliases.txt", + Data: map[string]string{ + "Harry Potter": "The Boy Who Lived", + "Tom Riddle": "Lord Voldemort", + }, + }), + }}, + } + + protorange.Range(m.ProtoReflect(), func(p protopath.Values) error { + const ( + sensitive = "Voldemort" + alternative = "[He-Who-Must-Not-Be-Named]" + ) + + // Check if there is a sensitive word to redact. + last := p.Index(-1) + s, ok := last.Value.Interface().(string) + if !ok || !strings.Contains(s, sensitive) { + return nil + } + s = strings.Replace(s, sensitive, alternative, -1) + + // Store the redacted string back into the message. + beforeLast := p.Index(-2) + switch last.Step.Kind() { + case protopath.FieldAccessStep: + m := beforeLast.Value.Message() + fd := last.Step.FieldDescriptor() + m.Set(fd, protoreflect.ValueOfString(s)) + case protopath.ListIndexStep: + ls := beforeLast.Value.List() + i := last.Step.ListIndex() + ls.Set(i, protoreflect.ValueOfString(s)) + case protopath.MapIndexStep: + ms := beforeLast.Value.Map() + k := last.Step.MapIndex() + ms.Set(k, protoreflect.ValueOfString(s)) + } + return nil + }) + + fmt.Println(protojson.Format(m)) + + // Output: + // { + // "author": "Hermione Granger", + // "date": "1998-05-02T00:00:00Z", + // "title": "Harry Potter vanquishes [He-Who-Must-Not-Be-Named] once and for all!", + // "content": "In a final duel between Harry Potter and Lord [He-Who-Must-Not-Be-Named] earlier this evening...", + // "tags": [ + // "HarryPotter", + // "Lord[He-Who-Must-Not-Be-Named]" + // ], + // "attachments": [ + // { + // "@type": "google.golang.org.KeyValueAttachment", + // "name": "aliases.txt", + // "data": { + // "Harry Potter": "The Boy Who Lived", + // "Tom Riddle": "Lord [He-Who-Must-Not-Be-Named]" + // } + // } + // ] + // } +} diff --git a/reflect/protorange/range.go b/reflect/protorange/range.go new file mode 100644 index 00000000..01750f77 --- /dev/null +++ b/reflect/protorange/range.go @@ -0,0 +1,315 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package protorange provides functionality to traverse a message value. +package protorange + +import ( + "bytes" + "errors" + + "google.golang.org/protobuf/internal/genid" + "google.golang.org/protobuf/internal/order" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protopath" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +var ( + // Break breaks traversal of children in the current value. + // It has no effect when traversing values that are not composite types + // (e.g., messages, lists, and maps). + Break = errors.New("break traversal of children in current value") + + // Terminate terminates the entire range operation. + // All necessary Pop operations continue to be called. + Terminate = errors.New("terminate range operation") +) + +// Range performs a depth-first traversal over reachable values in a message. +// +// See Options.Range for details. +func Range(m protoreflect.Message, f func(protopath.Values) error) error { + return Options{}.Range(m, f, nil) +} + +// Options configures traversal of a message value tree. +type Options struct { + // Stable specifies whether to visit message fields and map entries + // in a stable ordering. If false, then the ordering is undefined and + // may be non-deterministic. + // + // Message fields are visited in ascending order by field number. + // Map entries are visited in ascending order, where + // boolean keys are ordered such that false sorts before true, + // numeric keys are ordered based on the numeric value, and + // string keys are lexicographically ordered by Unicode codepoints. + Stable bool + + // Resolver is used for looking up types when expanding google.protobuf.Any + // messages. If nil, this defaults to using protoregistry.GlobalTypes. + // To prevent expansion of Any messages, pass an empty protoregistry.Types: + // + // Options{Resolver: (*protoregistry.Types)(nil)} + // + Resolver interface { + protoregistry.ExtensionTypeResolver + protoregistry.MessageTypeResolver + } +} + +// Range performs a depth-first traversal over reachable values in a message. +// The first push and the last pop are to push/pop a protopath.Root step. +// If push or pop return any non-nil error (other than Break or Terminate), +// it terminates the traversal and is returned by Range. +// +// The rules for traversing a message is as follows: +// +// • For messages, iterate over every populated known and extension field. +// Each field is preceded by a push of a protopath.FieldAccess step, +// followed by recursive application of the rules on the field value, +// and succeeded by a pop of that step. +// If the message has unknown fields, then push an protopath.UnknownAccess step +// followed immediately by pop of that step. +// +// • As an exception to the above rule, if the current message is a +// google.protobuf.Any message, expand the underlying message (if resolvable). +// The expanded message is preceded by a push of a protopath.AnyExpand step, +// followed by recursive application of the rules on the underlying message, +// and succeeded by a pop of that step. Mutations to the expanded message +// are written back to the Any message when popping back out. +// +// • For lists, iterate over every element. Each element is preceded by a push +// of a protopath.ListIndex step, followed by recursive application of the rules +// on the list element, and succeeded by a pop of that step. +// +// • For maps, iterate over every entry. Each entry is preceded by a push +// of a protopath.MapIndex step, followed by recursive application of the rules +// on the map entry value, and succeeded by a pop of that step. +// +// Mutations should only be made to the last value, otherwise the effects on +// traversal will be undefined. If the mutation is made to the last value +// during to a push, then the effects of the mutation will affect traversal. +// For example, if the last value is currently a message, and the push function +// populates a few fields in that message, then the newly modified fields +// will be traversed. +// +// The protopath.Values provided to push functions is only valid until the +// corresponding pop call and the values provided to a pop call is only valid +// for the duration of the pop call itself. +func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error { + var err error + p := new(protopath.Values) + if o.Resolver == nil { + o.Resolver = protoregistry.GlobalTypes + } + + pushStep(p, protopath.Root(m.Descriptor()), protoreflect.ValueOfMessage(m)) + if push != nil { + err = amendError(err, push(*p)) + } + if err == nil { + err = o.rangeMessage(p, m, push, pop) + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + + if err == Break || err == Terminate { + err = nil + } + return err +} + +func (o Options) rangeMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (err error) { + if ok, err := o.rangeAnyMessage(p, m, push, pop); ok { + return err + } + + fieldOrder := order.AnyFieldOrder + if o.Stable { + fieldOrder = order.NumberFieldOrder + } + order.RangeFields(m, fieldOrder, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + pushStep(p, protopath.FieldAccess(fd), v) + if push != nil { + err = amendError(err, push(*p)) + } + if err == nil { + switch { + case fd.IsMap(): + err = o.rangeMap(p, fd, v.Map(), push, pop) + case fd.IsList(): + err = o.rangeList(p, fd, v.List(), push, pop) + case fd.Message() != nil: + err = o.rangeMessage(p, v.Message(), push, pop) + } + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + return err == nil + }) + + if b := m.GetUnknown(); len(b) > 0 && err == nil { + pushStep(p, protopath.UnknownAccess(), protoreflect.ValueOfBytes(b)) + if push != nil { + err = amendError(err, push(*p)) + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + } + + if err == Break { + err = nil + } + return err +} + +func (o Options) rangeAnyMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (ok bool, err error) { + md := m.Descriptor() + if md.FullName() != "google.protobuf.Any" { + return false, nil + } + + fds := md.Fields() + url := m.Get(fds.ByNumber(genid.Any_TypeUrl_field_number)).String() + val := m.Get(fds.ByNumber(genid.Any_Value_field_number)).Bytes() + mt, errFind := o.Resolver.FindMessageByURL(url) + if errFind != nil { + return false, nil + } + + // Unmarshal the raw encoded message value into a structured message value. + m2 := mt.New() + errUnmarshal := proto.UnmarshalOptions{ + Merge: true, + AllowPartial: true, + Resolver: o.Resolver, + }.Unmarshal(val, m2.Interface()) + if errUnmarshal != nil { + // If the the underlying message cannot be unmarshaled, + // then just treat this as an normal message type. + return false, nil + } + + // Marshal Any before ranging to detect possible mutations. + b1, errMarshal := proto.MarshalOptions{ + AllowPartial: true, + Deterministic: true, + }.Marshal(m2.Interface()) + if errMarshal != nil { + return true, errMarshal + } + + pushStep(p, protopath.AnyExpand(m2.Descriptor()), protoreflect.ValueOfMessage(m2)) + if push != nil { + err = amendError(err, push(*p)) + } + if err == nil { + err = o.rangeMessage(p, m2, push, pop) + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + + // Marshal Any after ranging to detect possible mutations. + b2, errMarshal := proto.MarshalOptions{ + AllowPartial: true, + Deterministic: true, + }.Marshal(m2.Interface()) + if errMarshal != nil { + return true, errMarshal + } + + // Mutations detected, write the new sequence of bytes to the Any message. + if !bytes.Equal(b1, b2) { + m.Set(fds.ByNumber(genid.Any_Value_field_number), protoreflect.ValueOfBytes(b2)) + } + + if err == Break { + err = nil + } + return true, err +} + +func (o Options) rangeList(p *protopath.Values, fd protoreflect.FieldDescriptor, ls protoreflect.List, push, pop func(protopath.Values) error) (err error) { + for i := 0; i < ls.Len() && err == nil; i++ { + v := ls.Get(i) + pushStep(p, protopath.ListIndex(i), v) + if push != nil { + err = amendError(err, push(*p)) + } + if err == nil && fd.Message() != nil { + err = o.rangeMessage(p, v.Message(), push, pop) + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + } + + if err == Break { + err = nil + } + return err +} + +func (o Options) rangeMap(p *protopath.Values, fd protoreflect.FieldDescriptor, ms protoreflect.Map, push, pop func(protopath.Values) error) (err error) { + keyOrder := order.AnyKeyOrder + if o.Stable { + keyOrder = order.GenericKeyOrder + } + order.RangeEntries(ms, keyOrder, func(k protoreflect.MapKey, v protoreflect.Value) bool { + pushStep(p, protopath.MapIndex(k), v) + if push != nil { + err = amendError(err, push(*p)) + } + if err == nil && fd.MapValue().Message() != nil { + err = o.rangeMessage(p, v.Message(), push, pop) + } + if pop != nil { + err = amendError(err, pop(*p)) + } + popStep(p) + return err == nil + }) + + if err == Break { + err = nil + } + return err +} + +func pushStep(p *protopath.Values, s protopath.Step, v protoreflect.Value) { + p.Path = append(p.Path, s) + p.Values = append(p.Values, v) +} + +func popStep(p *protopath.Values) { + p.Path = p.Path[:len(p.Path)-1] + p.Values = p.Values[:len(p.Values)-1] +} + +// amendErrors amends the previous error with the current error if it is +// considered more serious. The precedence order for errors is: +// nil < Break < Terminate < previous non-nil < current non-nil +func amendError(prev, curr error) error { + switch { + case curr == nil: + return prev + case curr == Break && prev != nil: + return prev + case curr == Terminate && prev != nil && prev != Break: + return prev + default: + return curr + } +} diff --git a/reflect/protorange/range_test.go b/reflect/protorange/range_test.go new file mode 100644 index 00000000..a8ca6a0b --- /dev/null +++ b/reflect/protorange/range_test.go @@ -0,0 +1,253 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protorange + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protopath" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/testing/protocmp" + + newspb "google.golang.org/protobuf/internal/testprotos/news" + anypb "google.golang.org/protobuf/types/known/anypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +func mustMarshal(m proto.Message) []byte { + b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m) + if err != nil { + panic(err) + } + return b +} + +var transformReflectValue = cmp.Transformer("", func(v protoreflect.Value) interface{} { + switch v := v.Interface().(type) { + case protoreflect.Message: + return v.Interface() + case protoreflect.Map: + ms := map[interface{}]protoreflect.Value{} + v.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + ms[k.Interface()] = v + return true + }) + return ms + case protoreflect.List: + ls := []protoreflect.Value{} + for i := 0; i < v.Len(); i++ { + ls = append(ls, v.Get(i)) + } + return ls + default: + return v + } +}) + +func TestRange(t *testing.T) { + m2 := (&newspb.KeyValueAttachment{ + Name: "checksums.txt", + Data: map[string]string{ + "go1.10.src.tar.gz": "07cbb9d0091b846c6aea40bf5bc0cea7", + "go1.10.darwin-amd64.pkg": "cbb38bb6ff6ea86279e01745984445bf", + "go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566", + "go1.10.windows-amd64.msi": "57bda02030f58f5d2bf71943e1390123", + }, + }).ProtoReflect() + m := (&newspb.Article{ + Author: "Brad Fitzpatrick", + Date: timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)), + Title: "Go 1.10 is released", + Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...", + Status: newspb.Article_PUBLISHED, + Tags: []string{"go1.10", "release"}, + Attachments: []*anypb.Any{{ + TypeUrl: "google.golang.org.KeyValueAttachment", + Value: mustMarshal(m2.Interface()), + }}, + }).ProtoReflect() + + // Nil push and pop functions should not panic. + noop := func(protopath.Values) error { return nil } + Options{}.Range(m, nil, nil) + Options{}.Range(m, noop, nil) + Options{}.Range(m, nil, noop) + + getByName := func(m protoreflect.Message, s protoreflect.Name) protoreflect.Value { + fds := m.Descriptor().Fields() + return m.Get(fds.ByName(s)) + } + + wantPaths := []string{ + ``, + `.author`, + `.date`, + `.date.seconds`, + `.title`, + `.content`, + `.attachments`, + `.attachments[0]`, + `.attachments[0].(google.golang.org.KeyValueAttachment)`, + `.attachments[0].(google.golang.org.KeyValueAttachment).name`, + `.attachments[0].(google.golang.org.KeyValueAttachment).data`, + `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.darwin-amd64.pkg"]`, + `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.linux-amd64.tar.gz"]`, + `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.src.tar.gz"]`, + `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.windows-amd64.msi"]`, + `.tags`, + `.tags[0]`, + `.tags[1]`, + `.status`, + } + wantValues := []protoreflect.Value{ + protoreflect.ValueOfMessage(m), + getByName(m, "author"), + getByName(m, "date"), + getByName(getByName(m, "date").Message(), "seconds"), + getByName(m, `title`), + getByName(m, `content`), + getByName(m, `attachments`), + getByName(m, `attachments`).List().Get(0), + protoreflect.ValueOfMessage(m2), + getByName(m2, `name`), + getByName(m2, `data`), + getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.darwin-amd64.pkg").MapKey()), + getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.linux-amd64.tar.gz").MapKey()), + getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.src.tar.gz").MapKey()), + getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.windows-amd64.msi").MapKey()), + getByName(m, `tags`), + getByName(m, `tags`).List().Get(0), + getByName(m, `tags`).List().Get(1), + getByName(m, `status`), + } + + tests := []struct { + resolver interface { + protoregistry.ExtensionTypeResolver + protoregistry.MessageTypeResolver + } + + errorAt int + breakAt int + terminateAt int + + wantPaths []string + wantValues []protoreflect.Value + wantError error + }{{ + wantPaths: wantPaths, + wantValues: wantValues, + }, { + resolver: (*protoregistry.Types)(nil), + wantPaths: append(append(wantPaths[:8:8], + `.attachments[0].type_url`, + `.attachments[0].value`, + ), wantPaths[15:]...), + wantValues: append(append(wantValues[:8:8], + protoreflect.ValueOfString("google.golang.org.KeyValueAttachment"), + protoreflect.ValueOfBytes(mustMarshal(m2.Interface())), + ), wantValues[15:]...), + }, { + errorAt: 5, // return error within newspb.Article + wantPaths: wantPaths[:5], + wantValues: wantValues[:5], + wantError: cmpopts.AnyError, + }, { + terminateAt: 11, // terminate within newspb.KeyValueAttachment + wantPaths: wantPaths[:11], + wantValues: wantValues[:11], + }, { + breakAt: 11, // break within newspb.KeyValueAttachment + wantPaths: append(wantPaths[:11:11], wantPaths[15:]...), + wantValues: append(wantValues[:11:11], wantValues[15:]...), + }, { + errorAt: 17, // return error within newspb.Article.Tags + wantPaths: wantPaths[:17], + wantValues: wantValues[:17], + wantError: cmpopts.AnyError, + }, { + breakAt: 17, // break within newspb.Article.Tags + wantPaths: append(wantPaths[:17:17], wantPaths[18:]...), + wantValues: append(wantValues[:17:17], wantValues[18:]...), + }, { + terminateAt: 17, // terminate within newspb.Article.Tags + wantPaths: wantPaths[:17], + wantValues: wantValues[:17], + }, { + errorAt: 13, // return error within newspb.KeyValueAttachment.Data + wantPaths: wantPaths[:13], + wantValues: wantValues[:13], + wantError: cmpopts.AnyError, + }, { + breakAt: 13, // break within newspb.KeyValueAttachment.Data + wantPaths: append(wantPaths[:13:13], wantPaths[15:]...), + wantValues: append(wantValues[:13:13], wantValues[15:]...), + }, { + terminateAt: 13, // terminate within newspb.KeyValueAttachment.Data + wantPaths: wantPaths[:13], + wantValues: wantValues[:13], + }} + for _, tt := range tests { + t.Run("", func(t *testing.T) { + var gotPaths []string + var gotValues []protoreflect.Value + var stackPaths []string + var stackValues []protoreflect.Value + gotError := Options{ + Stable: true, + Resolver: tt.resolver, + }.Range(m, + func(p protopath.Values) error { + gotPaths = append(gotPaths, p.Path[1:].String()) + stackPaths = append(stackPaths, p.Path[1:].String()) + gotValues = append(gotValues, p.Index(-1).Value) + stackValues = append(stackValues, p.Index(-1).Value) + switch { + case tt.errorAt > 0 && tt.errorAt == len(gotPaths): + return cmpopts.AnyError + case tt.breakAt > 0 && tt.breakAt == len(gotPaths): + return Break + case tt.terminateAt > 0 && tt.terminateAt == len(gotPaths): + return Terminate + default: + return nil + } + }, + func(p protopath.Values) error { + gotPath := p.Path[1:].String() + wantPath := stackPaths[len(stackPaths)-1] + if wantPath != gotPath { + t.Errorf("%d: pop path mismatch: got %v, want %v", len(gotPaths), gotPath, wantPath) + } + gotValue := p.Index(-1).Value + wantValue := stackValues[len(stackValues)-1] + if diff := cmp.Diff(wantValue, gotValue, transformReflectValue, protocmp.Transform()); diff != "" { + t.Errorf("%d: pop value mismatch (-want +got):\n%v", len(gotValues), diff) + } + stackPaths = stackPaths[:len(stackPaths)-1] + stackValues = stackValues[:len(stackValues)-1] + return nil + }, + ) + if n := len(stackPaths) + len(stackValues); n > 0 { + t.Errorf("stack mismatch: got %d unpopped items", n) + } + if diff := cmp.Diff(tt.wantPaths, gotPaths); diff != "" { + t.Errorf("paths mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantValues, gotValues, transformReflectValue, protocmp.Transform()); diff != "" { + t.Errorf("values mismatch (-want +got):\n%s", diff) + } + if !cmp.Equal(gotError, tt.wantError, cmpopts.EquateErrors()) { + t.Errorf("error mismatch: got %v, want %v", gotError, tt.wantError) + } + }) + } +}