// 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]" // } // } // ] // } }