Repository: kythe/kythe
Pull request #4347 (2020-02-03 23:34:55)

feat(tooling): add vnames utility for handling rewrite rules


Patch:
diff --git a/kythe/go/util/tools/vnames/BUILD b/kythe/go/util/tools/vnames/BUILD
new file mode 100644
index 000000000..4dbc0b395
--- /dev/null
+++ b/kythe/go/util/tools/vnames/BUILD
@@ -0,0 +1,20 @@
+load("//tools:build_rules/shims.bzl", "go_binary")
+
+package(default_visibility = ["//kythe:default_visibility"])
+
+go_binary(
+    name = "vnames",
+    srcs = [
+        "apply.go",
+        "convert.go",
+        "vnames.go",
+    ],
+    deps = [
+        "//kythe/go/platform/vfs",
+        "//kythe/go/util/cmdutil",
+        "//kythe/go/util/vnameutil",
+        "//kythe/proto:storage_go_proto",
+        "@com_github_golang_protobuf//jsonpb:go_default_library_gen",
+        "@com_github_google_subcommands//:go_default_library",
+    ],
+)
diff --git a/kythe/go/util/tools/vnames/apply.go b/kythe/go/util/tools/vnames/apply.go
new file mode 100644
index 000000000..479f8d089
--- /dev/null
+++ b/kythe/go/util/tools/vnames/apply.go
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020 The Kythe Authors. All rights reserved.
+ *
+ * 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 main
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+
+	"kythe.io/kythe/go/platform/vfs"
+	"kythe.io/kythe/go/util/cmdutil"
+	"kythe.io/kythe/go/util/vnameutil"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/google/subcommands"
+)
+
+type rulesFormat string
+
+const (
+	jsonFormat  rulesFormat = "JSON"
+	protoFormat rulesFormat = "PROTO"
+)
+
+type applyRulesCmd struct {
+	cmdutil.Info
+
+	rulesPath string
+	format    string
+
+	allowEmpty bool
+}
+
+var applyRulesInfo = cmdutil.NewInfo("apply-rules", "apply VName rewrite rules to stdin lines",
+	`Usage: apply-rules --rules <path> [--format proto]`)
+
+func (c *applyRulesCmd) SetFlags(flag *flag.FlagSet) {
+	flag.StringVar(&c.rulesPath, "rules", "", "Path to VName rewrite rules file")
+	flag.StringVar(&c.format, "format", string(jsonFormat), `Path of VName rewrite rules file {"JSON", "PROTO"}`)
+	flag.BoolVar(&c.allowEmpty, "allow_empty", false, "Whether to write an empty VName when no rule matches the input")
+}
+func (c *applyRulesCmd) Execute(ctx context.Context, flag *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+	rules, err := rulesFormat(strings.ToUpper(c.format)).readFile(ctx, c.rulesPath)
+	if err != nil {
+		return cmdErrorf("reading %q: %v", c.rulesPath, err)
+	}
+	s := bufio.NewScanner(os.Stdin)
+	m := jsonpb.Marshaler{OrigName: true}
+	for s.Scan() {
+		line := s.Text()
+		v, ok := rules.Apply(line)
+		if ok || c.allowEmpty {
+			if err := m.Marshal(os.Stdout, v); err != nil {
+				return cmdErrorf("writing VName: %v", err)
+			} else if _, err := fmt.Fprintln(os.Stdout, ""); err != nil {
+				return cmdErrorf("writing newline: %v", err)
+			}
+		}
+	}
+	if err := s.Err(); err != nil {
+		return cmdErrorf("reading paths from stdin: %v", err)
+	}
+	return subcommands.ExitSuccess
+}
+
+func (f rulesFormat) readFile(ctx context.Context, path string) (vnameutil.Rules, error) {
+	file, err := vfs.Open(ctx, path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+	return f.readRules(file)
+}
+
+func (f rulesFormat) readRules(r io.Reader) (vnameutil.Rules, error) {
+	switch f {
+	case jsonFormat:
+		return vnameutil.ReadRules(r)
+	case protoFormat:
+		rec, err := ioutil.ReadAll(r)
+		if err != nil {
+			return nil, err
+		}
+		return vnameutil.ParseProtoRules(rec)
+	default:
+		return nil, fmt.Errorf("unknown rules format: %q", f)
+	}
+}
+
+func (f rulesFormat) writeRules(rules vnameutil.Rules, w io.Writer) error {
+	switch f {
+	case jsonFormat:
+		return json.NewEncoder(w).Encode(rules)
+	case protoFormat:
+		rec, err := rules.Marshal()
+		if err != nil {
+			return err
+		}
+		_, err = w.Write(rec)
+		return err
+	default:
+		return fmt.Errorf("unknown rules format: %q", f)
+	}
+}
+
+func cmdErrorf(msg string, args ...interface{}) subcommands.ExitStatus {
+	log.Printf("ERROR: "+msg, args...)
+	return subcommands.ExitFailure
+}
diff --git a/kythe/go/util/tools/vnames/convert.go b/kythe/go/util/tools/vnames/convert.go
new file mode 100644
index 000000000..1953e15c8
--- /dev/null
+++ b/kythe/go/util/tools/vnames/convert.go
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Kythe Authors. All rights reserved.
+ *
+ * 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 main
+
+import (
+	"context"
+	"flag"
+	"os"
+	"strings"
+
+	"github.com/google/subcommands"
+	"kythe.io/kythe/go/util/cmdutil"
+)
+
+type convertRulesCmd struct {
+	cmdutil.Info
+
+	fromFormat, toFormat string
+}
+
+var convertRulesInfo = cmdutil.NewInfo("convert-rules", `convert VName rewrite rules (formats: {"JSON", "PROTO"})`,
+	`Usage: convert-rules --from <format> --to <format>`)
+
+func (c *convertRulesCmd) SetFlags(flag *flag.FlagSet) {
+	flag.StringVar(&c.fromFormat, "from", "", "Source format of VName rewrite rules")
+	flag.StringVar(&c.toFormat, "to", "", "Target format of VName rewrite rules")
+}
+func (c *convertRulesCmd) Execute(ctx context.Context, flag *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+	if c.fromFormat == "" {
+		return cmdErrorf("--from <format> must be specified")
+	} else if c.toFormat == "" {
+		return cmdErrorf("--to <format> must be specified")
+	}
+
+	rules, err := rulesFormat(strings.ToUpper(c.fromFormat)).readRules(os.Stdin)
+	if err != nil {
+		return cmdErrorf("reading rules: %v", err)
+	} else if err := rulesFormat(strings.ToUpper(c.toFormat)).writeRules(rules, os.Stdout); err != nil {
+		return cmdErrorf("writing rules: %v", err)
+	}
+	return subcommands.ExitSuccess
+}
diff --git a/kythe/go/util/tools/vnames/vnames.go b/kythe/go/util/tools/vnames/vnames.go
new file mode 100644
index 000000000..ff6b58a15
--- /dev/null
+++ b/kythe/go/util/tools/vnames/vnames.go
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Kythe Authors. All rights reserved.
+ *
+ * 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.
+ */
+
+// Binary vnames is a utility for handling VNames.
+//
+// Examples:
+//   # Convert VName rewrite rules from json to proto.
+//   vnames convert-rules --from json --to proto < rules.json > rules.pb.bin
+//
+//   # Apply VName rewrite rules to stdin.
+//   print -l path1 path2 path3 | vnames apply-rules --rules rules.json
+package main
+
+import (
+	"context"
+	"flag"
+	"os"
+
+	"github.com/google/subcommands"
+)
+
+func main() {
+	flag.Parse()
+
+	subcommands.Register(&applyRulesCmd{Info: applyRulesInfo}, "rules")
+	subcommands.Register(&convertRulesCmd{Info: convertRulesInfo}, "rules")
+
+	subcommands.Register(subcommands.FlagsCommand(), "info")
+	subcommands.Register(subcommands.HelpCommand(), "info")
+
+	ctx := context.Background()
+	os.Exit(int(subcommands.Execute(ctx)))
+}
diff --git a/kythe/go/util/vnameutil/BUILD b/kythe/go/util/vnameutil/BUILD
index 4fe349e0c..674145c13 100644
--- a/kythe/go/util/vnameutil/BUILD
+++ b/kythe/go/util/vnameutil/BUILD
@@ -13,6 +13,7 @@ go_library(
     deps = [
         "//kythe/proto:storage_go_proto",
         "@com_github_golang_protobuf//jsonpb:go_default_library_gen",
+        "@com_github_golang_protobuf//proto:go_default_library",
     ],
 )
 
@@ -23,7 +24,6 @@ go_test(
     library = "vnameutil",
     deps = [
         "//kythe/proto:storage_go_proto",
-        "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_google_go_cmp//cmp:go_default_library",
     ],
 )
diff --git a/kythe/go/util/vnameutil/rewrite.go b/kythe/go/util/vnameutil/rewrite.go
index 2c4e34bb6..2e5d099c4 100644
--- a/kythe/go/util/vnameutil/rewrite.go
+++ b/kythe/go/util/vnameutil/rewrite.go
@@ -28,6 +28,7 @@ import (
 	"strings"
 
 	"github.com/golang/protobuf/jsonpb"
+	"github.com/golang/protobuf/proto"
 
 	spb "kythe.io/kythe/proto/storage_go_proto"
 )
@@ -126,6 +127,20 @@ func (r Rules) ApplyDefault(input string, v *spb.VName) *spb.VName {
 	return v
 }
 
+// ToProto returns an equivalent VNameRewriteRules proto.
+func (r Rules) ToProto() *spb.VNameRewriteRules {
+	pb := &spb.VNameRewriteRules{
+		Rule: make([]*spb.VNameRewriteRule, len(r)),
+	}
+	for i, rule := range r {
+		pb.Rule[i] = rule.ToProto()
+	}
+	return pb
+}
+
+// Marshal implements the proto.Marshaler interface.
+func (r Rules) Marshal() ([]byte, error) { return proto.Marshal(r.ToProto()) }
+
 // ConvertRule compiles a VNameRewriteRule proto into a Rule that can be applied to strings.
 func ConvertRule(r *spb.VNameRewriteRule) (Rule, error) {
 	pattern := "^" + strings.TrimSuffix(strings.TrimPrefix(r.Pattern, "^"), "$") + "$"
@@ -180,6 +195,23 @@ func expectDelim(de *json.Decoder, expected json.Delim) error {
 	return nil
 }
 
+// ParseProtoRules reads a wire-encoded *spb.VNameRewriteRules.
+func ParseProtoRules(data []byte) (Rules, error) {
+	var pb spb.VNameRewriteRules
+	if err := proto.Unmarshal(data, &pb); err != nil {
+		return nil, err
+	}
+	rules := make(Rules, len(pb.Rule))
+	for i, rp := range pb.Rule {
+		r, err := ConvertRule(rp)
+		if err != nil {
+			return nil, err
+		}
+		rules[i] = r
+	}
+	return rules, nil
+}
+
 // ParseRules reads Rules from JSON-encoded data in a byte array.
 func ParseRules(data []byte) (Rules, error) { return ReadRules(bytes.NewReader(data)) }
 
diff --git a/kythe/proto/storage.proto b/kythe/proto/storage.proto
index 7ae67d919..eccbfcdbd 100644
--- a/kythe/proto/storage.proto
+++ b/kythe/proto/storage.proto
@@ -198,3 +198,8 @@ message VNameRewriteRule {
   // n'th regexp group on a successful input match.
   VName v_name = 2 [json_name = "vname"];
 }
+
+// VNameRewriteRules is a container for multiple VNameRewriteRules.
+message VNameRewriteRules {
+  repeated VNameRewriteRule rule = 1;
+}
diff --git a/kythe/proto/storage_go_proto/storage.pb.go b/kythe/proto/storage_go_proto/storage.pb.go
index 246075e14..6ee13c3fc 100644
--- a/kythe/proto/storage_go_proto/storage.pb.go
+++ b/kythe/proto/storage_go_proto/storage.pb.go
@@ -695,6 +695,45 @@ func (m *VNameRewriteRule) GetVName() *VName {
 	return nil
 }
 
+type VNameRewriteRules struct {
+	Rule                 []*VNameRewriteRule `protobuf:"bytes,1,rep,name=rule,proto3" json:"rule,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
+	XXX_unrecognized     []byte              `json:"-"`
+	XXX_sizecache        int32               `json:"-"`
+}
+
+func (m *VNameRewriteRules) Reset()         { *m = VNameRewriteRules{} }
+func (m *VNameRewriteRules) String() string { return proto.CompactTextString(m) }
+func (*VNameRewriteRules) ProtoMessage()    {}
+func (*VNameRewriteRules) Descriptor() ([]byte, []int) {
+	return fileDescriptor_b1438903ecc9440e, []int{12}
+}
+
+func (m *VNameRewriteRules) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_VNameRewriteRules.Unmarshal(m, b)
+}
+func (m *VNameRewriteRules) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_VNameRewriteRules.Marshal(b, m, deterministic)
+}
+func (m *VNameRewriteRules) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_VNameRewriteRules.Merge(m, src)
+}
+func (m *VNameRewriteRules) XXX_Size() int {
+	return xxx_messageInfo_VNameRewriteRules.Size(m)
+}
+func (m *VNameRewriteRules) XXX_DiscardUnknown() {
+	xxx_messageInfo_VNameRewriteRules.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_VNameRewriteRules proto.InternalMessageInfo
+
+func (m *VNameRewriteRules) GetRule() []*VNameRewriteRule {
+	if m != nil {
+		return m.Rule
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*VName)(nil), "kythe.proto.VName")
 	proto.RegisterType((*VNameMask)(nil), "kythe.proto.VNameMask")
@@ -709,44 +748,46 @@ func init() {
 	proto.RegisterType((*CountReply)(nil), "kythe.proto.CountReply")
 	proto.RegisterType((*ShardRequest)(nil), "kythe.proto.ShardRequest")
 	proto.RegisterType((*VNameRewriteRule)(nil), "kythe.proto.VNameRewriteRule")
+	proto.RegisterType((*VNameRewriteRules)(nil), "kythe.proto.VNameRewriteRules")
 }
 
 func init() { proto.RegisterFile("kythe/proto/storage.proto", fileDescriptor_b1438903ecc9440e) }
 
 var fileDescriptor_b1438903ecc9440e = []byte{
-	// 531 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xcf, 0x6f, 0xd3, 0x30,
-	0x14, 0xc7, 0x95, 0xfe, 0xc8, 0xd2, 0xd7, 0x1e, 0x26, 0x0b, 0xa1, 0x30, 0x40, 0xab, 0x72, 0x40,
-	0x80, 0x50, 0x26, 0xb1, 0x03, 0x1c, 0x76, 0x02, 0x71, 0x42, 0x20, 0x94, 0x89, 0x4e, 0xe2, 0x52,
-	0x99, 0xe4, 0x2d, 0x8d, 0x9a, 0xda, 0xc1, 0x71, 0xba, 0xf5, 0x88, 0x04, 0x47, 0xfe, 0x1f, 0xfe,
-	0x3c, 0xe4, 0x67, 0x77, 0x64, 0x6c, 0x41, 0x30, 0xed, 0x54, 0xbf, 0xaf, 0xbf, 0x79, 0xef, 0xe3,
-	0xf7, 0xec, 0xc2, 0xbd, 0xe5, 0x46, 0x2f, 0xf0, 0xa0, 0x52, 0x52, 0xcb, 0x83, 0x5a, 0x4b, 0xc5,
-	0x73, 0x8c, 0x29, 0x62, 0x63, 0xda, 0xb2, 0x41, 0xf4, 0xd5, 0x83, 0xe1, 0xec, 0x3d, 0x5f, 0x21,
-	0x7b, 0x00, 0xa3, 0xba, 0xc8, 0x05, 0xd7, 0x8d, 0xc2, 0xd0, 0x9b, 0x7a, 0x8f, 0x47, 0xc9, 0x6f,
-	0x81, 0xdd, 0x05, 0x3f, 0x95, 0xaa, 0x6a, 0xea, 0xb0, 0x47, 0x5b, 0x2e, 0x62, 0x0c, 0x06, 0x4a,
-	0x4a, 0x1d, 0xf6, 0x49, 0xa5, 0xb5, 0xd1, 0x2a, 0xae, 0x17, 0xe1, 0xc0, 0x6a, 0x66, 0xcd, 0xf6,
-	0x20, 0x28, 0xb9, 0xc8, 0x1b, 0x9e, 0x63, 0x38, 0x24, 0xfd, 0x22, 0x8e, 0xbe, 0x7b, 0x30, 0x22,
-	0x86, 0x77, 0xbc, 0x5e, 0x5e, 0xe5, 0x08, 0xba, 0x39, 0x82, 0x6b, 0x39, 0x82, 0x6b, 0x38, 0x82,
-	0x0e, 0x8e, 0xa0, 0xc5, 0xf1, 0xd3, 0x83, 0xe1, 0x1b, 0xa1, 0xd5, 0x86, 0x3d, 0x05, 0xbf, 0x96,
-	0x8d, 0x4a, 0x2d, 0xc0, 0xf8, 0x39, 0x8b, 0x5b, 0x3d, 0x8b, 0x89, 0x35, 0x71, 0x0e, 0x76, 0x1f,
-	0x46, 0x98, 0xe5, 0x38, 0x5f, 0x16, 0x22, 0x73, 0xcd, 0x09, 0x8c, 0xf0, 0xb6, 0x10, 0x99, 0x49,
-	0xa4, 0xb9, 0xca, 0xd1, 0x82, 0x75, 0x24, 0xb2, 0x0e, 0x93, 0xe8, 0x94, 0xa7, 0x7a, 0x2e, 0xf8,
-	0x0a, 0x5d, 0xef, 0x02, 0x23, 0xd0, 0x74, 0x1e, 0x02, 0xd0, 0xe6, 0x9a, 0x97, 0x8d, 0x25, 0x9f,
-	0x24, 0x64, 0x9f, 0x19, 0x21, 0x7a, 0x01, 0x3b, 0x86, 0xbc, 0xc0, 0x9a, 0x3d, 0x83, 0x1d, 0xb4,
-	0xcb, 0xd0, 0x9b, 0xf6, 0xaf, 0xd4, 0xa4, 0x03, 0x26, 0x5b, 0x4b, 0x34, 0x83, 0x71, 0x82, 0x3c,
-	0x4b, 0xf0, 0x4b, 0x83, 0xb5, 0xbe, 0xb5, 0x83, 0x47, 0xdf, 0x7a, 0x30, 0x39, 0x51, 0x85, 0xc6,
-	0x9b, 0x64, 0x7e, 0x09, 0x7e, 0x53, 0x65, 0x5c, 0x63, 0xd8, 0xa3, 0x13, 0x4c, 0x2f, 0x79, 0xdb,
-	0x69, 0xe3, 0x8f, 0xe4, 0x4b, 0x9c, 0x7f, 0xef, 0x87, 0x07, 0xbe, 0x95, 0x2e, 0xe3, 0x79, 0x9d,
-	0x73, 0xe9, 0xfd, 0xdf, 0x5c, 0xfa, 0x7f, 0x9d, 0xcb, 0xe0, 0xcf, 0xb9, 0x4c, 0x00, 0x1c, 0x6e,
-	0x55, 0x6e, 0xa2, 0x33, 0x18, 0x1f, 0xa7, 0x5c, 0xb4, 0x5a, 0xe2, 0x20, 0xbc, 0x7f, 0x81, 0xe8,
-	0xbe, 0x65, 0xfb, 0x30, 0x26, 0x88, 0x4a, 0xe1, 0x69, 0x71, 0xee, 0x18, 0x89, 0xeb, 0x03, 0x29,
-	0xd1, 0x11, 0x4c, 0x5e, 0xcb, 0x46, 0xe8, 0x6d, 0xe5, 0x3b, 0x30, 0x2c, 0x44, 0x86, 0xe7, 0x54,
-	0xb8, 0x9f, 0xd8, 0xc0, 0xbc, 0xad, 0x7a, 0xc1, 0x55, 0x66, 0xdf, 0x56, 0x3f, 0x71, 0x51, 0xf4,
-	0x08, 0xc0, 0x7d, 0x5d, 0x95, 0x1b, 0x16, 0xb6, 0xef, 0x97, 0xb1, 0x5d, 0xdc, 0xa5, 0x23, 0x98,
-	0x1c, 0x9b, 0x2f, 0x6e, 0x56, 0xe5, 0x04, 0x76, 0xed, 0x91, 0xf1, 0x8c, 0x3a, 0xd6, 0x94, 0x68,
-	0x6a, 0x55, 0x5c, 0x6b, 0x54, 0xc2, 0x4d, 0x70, 0x1b, 0xb2, 0x27, 0xe0, 0xaf, 0xed, 0x44, 0xba,
-	0x07, 0x38, 0x5c, 0x1b, 0xc3, 0xab, 0x43, 0xd8, 0x4f, 0xe5, 0x2a, 0xce, 0xa5, 0xcc, 0x4b, 0x8c,
-	0x33, 0x5c, 0x6b, 0x29, 0xcb, 0xba, 0xed, 0xff, 0xb4, 0xeb, 0xfe, 0x21, 0xe7, 0xb9, 0x9c, 0x93,
-	0xf2, 0xd9, 0xa7, 0x9f, 0xc3, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xa5, 0xfa, 0x91, 0x30, 0x48,
-	0x05, 0x00, 0x00,
+	// 554 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xcf, 0x6e, 0xd3, 0x40,
+	0x10, 0xc6, 0xe5, 0xfc, 0x71, 0x9d, 0x49, 0x0e, 0x65, 0x85, 0x90, 0x29, 0x54, 0x8d, 0x7c, 0x40,
+	0x80, 0x90, 0x2b, 0xe8, 0x01, 0x0e, 0x3d, 0x81, 0xe0, 0x82, 0x40, 0xc8, 0x15, 0xa9, 0xc4, 0x25,
+	0x5a, 0xec, 0xa9, 0x63, 0xc5, 0xf1, 0x9a, 0xf5, 0x3a, 0x6d, 0x8e, 0x48, 0x70, 0xe4, 0x7d, 0x78,
+	0x3c, 0xb4, 0xb3, 0x9b, 0xd6, 0x6d, 0x63, 0x04, 0x55, 0x4f, 0xf1, 0x7c, 0xfb, 0xed, 0xcc, 0x6f,
+	0x67, 0x76, 0x03, 0xf7, 0xe7, 0x2b, 0x35, 0xc3, 0xfd, 0x52, 0x0a, 0x25, 0xf6, 0x2b, 0x25, 0x24,
+	0x4f, 0x31, 0xa4, 0x88, 0x0d, 0x69, 0xc9, 0x04, 0xc1, 0x77, 0x07, 0xfa, 0x93, 0x8f, 0x7c, 0x81,
+	0xec, 0x21, 0x0c, 0xaa, 0x2c, 0x2d, 0xb8, 0xaa, 0x25, 0xfa, 0xce, 0xd8, 0x79, 0x3c, 0x88, 0x2e,
+	0x04, 0x76, 0x0f, 0xdc, 0x58, 0xc8, 0xb2, 0xae, 0xfc, 0x0e, 0x2d, 0xd9, 0x88, 0x31, 0xe8, 0x49,
+	0x21, 0x94, 0xdf, 0x25, 0x95, 0xbe, 0xb5, 0x56, 0x72, 0x35, 0xf3, 0x7b, 0x46, 0xd3, 0xdf, 0x6c,
+	0x07, 0xbc, 0x9c, 0x17, 0x69, 0xcd, 0x53, 0xf4, 0xfb, 0xa4, 0x9f, 0xc7, 0xc1, 0x4f, 0x07, 0x06,
+	0xc4, 0xf0, 0x81, 0x57, 0xf3, 0xeb, 0x1c, 0x5e, 0x3b, 0x87, 0xb7, 0x91, 0xc3, 0xdb, 0xc0, 0xe1,
+	0xb5, 0x70, 0x78, 0x0d, 0x8e, 0xdf, 0x0e, 0xf4, 0xdf, 0x16, 0x4a, 0xae, 0xd8, 0x53, 0x70, 0x2b,
+	0x51, 0xcb, 0xd8, 0x00, 0x0c, 0x5f, 0xb0, 0xb0, 0xd1, 0xb3, 0x90, 0x58, 0x23, 0xeb, 0x60, 0x0f,
+	0x60, 0x80, 0x49, 0x8a, 0xd3, 0x79, 0x56, 0x24, 0xb6, 0x39, 0x9e, 0x16, 0xde, 0x67, 0x45, 0xa2,
+	0x13, 0x29, 0x2e, 0x53, 0x34, 0x60, 0x2d, 0x89, 0x8c, 0x43, 0x27, 0x3a, 0xe1, 0xb1, 0x9a, 0x16,
+	0x7c, 0x81, 0xb6, 0x77, 0x9e, 0x16, 0x68, 0x3a, 0xbb, 0x00, 0xb4, 0xb8, 0xe4, 0x79, 0x6d, 0xc8,
+	0x47, 0x11, 0xd9, 0x27, 0x5a, 0x08, 0x5e, 0xc2, 0x96, 0x26, 0xcf, 0xb0, 0x62, 0xcf, 0x60, 0x0b,
+	0xcd, 0xa7, 0xef, 0x8c, 0xbb, 0xd7, 0x6a, 0xd2, 0x01, 0xa3, 0xb5, 0x25, 0x98, 0xc0, 0x30, 0x42,
+	0x9e, 0x44, 0xf8, 0xad, 0xc6, 0x4a, 0xdd, 0xda, 0xc1, 0x83, 0x1f, 0x1d, 0x18, 0x1d, 0xcb, 0x4c,
+	0xe1, 0x4d, 0x32, 0xbf, 0x02, 0xb7, 0x2e, 0x13, 0xae, 0xd0, 0xef, 0xd0, 0x09, 0xc6, 0x97, 0xbc,
+	0xcd, 0xb4, 0xe1, 0x67, 0xf2, 0x45, 0xd6, 0xbf, 0xf3, 0xcb, 0x01, 0xd7, 0x48, 0x97, 0xf1, 0x9c,
+	0xd6, 0xb9, 0x74, 0xfe, 0x6f, 0x2e, 0xdd, 0xbf, 0xce, 0xa5, 0x77, 0x75, 0x2e, 0x23, 0x00, 0x8b,
+	0x5b, 0xe6, 0xab, 0xe0, 0x14, 0x86, 0x47, 0x31, 0x2f, 0x1a, 0x2d, 0xb1, 0x10, 0xce, 0xbf, 0x40,
+	0xb4, 0xdf, 0xb2, 0x3d, 0x18, 0x12, 0x44, 0x29, 0xf1, 0x24, 0x3b, 0xb3, 0x8c, 0xc4, 0xf5, 0x89,
+	0x94, 0xe0, 0x10, 0x46, 0x6f, 0x44, 0x5d, 0xa8, 0x75, 0xe5, 0xbb, 0xd0, 0xcf, 0x8a, 0x04, 0xcf,
+	0xa8, 0x70, 0x37, 0x32, 0x81, 0x7e, 0x5b, 0xd5, 0x8c, 0xcb, 0xc4, 0xbc, 0xad, 0x6e, 0x64, 0xa3,
+	0xe0, 0x11, 0x80, 0xdd, 0x5d, 0xe6, 0x2b, 0xe6, 0x37, 0xef, 0x97, 0xb6, 0x9d, 0xdf, 0xa5, 0x43,
+	0x18, 0x1d, 0xe9, 0x1d, 0x37, 0xab, 0x72, 0x0c, 0xdb, 0xe6, 0xc8, 0x78, 0x4a, 0x1d, 0xab, 0x73,
+	0xd4, 0xb5, 0x4a, 0xae, 0x14, 0xca, 0xc2, 0x4e, 0x70, 0x1d, 0xb2, 0x27, 0xe0, 0x2e, 0xcd, 0x44,
+	0xda, 0x07, 0xd8, 0x5f, 0x6a, 0x43, 0xf0, 0x0e, 0xee, 0x5c, 0x4d, 0x5c, 0xb1, 0xe7, 0xd0, 0x93,
+	0x75, 0x8e, 0xf6, 0x89, 0xec, 0x6e, 0xd8, 0x7d, 0xe1, 0x8e, 0xc8, 0xfa, 0xfa, 0x00, 0xf6, 0x62,
+	0xb1, 0x08, 0x53, 0x21, 0xd2, 0x1c, 0xc3, 0x04, 0x97, 0x4a, 0x88, 0xbc, 0x6a, 0xee, 0xfc, 0xb2,
+	0x6d, 0xff, 0x69, 0xa7, 0xa9, 0x98, 0x92, 0xf2, 0xd5, 0xa5, 0x9f, 0x83, 0x3f, 0x01, 0x00, 0x00,
+	0xff, 0xff, 0x4b, 0x02, 0x56, 0xfd, 0x90, 0x05, 0x00, 0x00,
 }


Test patch:
diff --git a/kythe/go/util/vnameutil/rewrite_test.go b/kythe/go/util/vnameutil/rewrite_test.go
index 939874c97..a9119f707 100644
--- a/kythe/go/util/vnameutil/rewrite_test.go
+++ b/kythe/go/util/vnameutil/rewrite_test.go
@@ -118,6 +118,7 @@ func TestRoundtripJSON(t *testing.T) {
 				t.Fatal(err)
 			}
 			t.Logf("Rules: %s", expected)
+
 			rec, err := json.Marshal(expected)
 			if err != nil {
 				t.Fatalf("Error marshaling rules %+v: %v", expected, err)
@@ -135,6 +136,42 @@ func TestRoundtripJSON(t *testing.T) {
 	}
 }
 
+func TestRoundtripProto(t *testing.T) {
+	tests := []string{
+		"[]",
+		`[{"pattern": "p(.)", "vname": {"corpus": "$@1@"}}]`,
+		`[{
+  "pattern": "(?P<corpus>.+)/(?P<root>.+)::(?P<path>.+)",
+  "vname": {"corpus": "@corpus@", "root": "@root@", "path": "@path@"}
+}]`,
+		testConfig,
+	}
+
+	for i, test := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			expected, err := ReadRules(strings.NewReader(test))
+			if err != nil {
+				t.Fatal(err)
+			}
+			t.Logf("Rules: %s", expected)
+
+			rec, err := expected.Marshal()
+			if err != nil {
+				t.Fatalf("Error marshaling rules %+v: %v", expected, err)
+			}
+
+			r, err := ParseProtoRules(rec)
+			if err != nil {
+				t.Fatalf("Error parsing rules %q: %v", rec, err)
+			}
+
+			if diff := cmp.Diff(r, expected, transformRegexp); diff != "" {
+				t.Errorf("Unexpected diff (- found; + expected):\n%s", diff)
+			}
+		})
+	}
+}
+
 func TestCornerCases(t *testing.T) {
 	testRule1 := Rule{regexp.MustCompile(`(?P<first>\w+)(?:/(?P<second>\w+))?`), V{Corpus: "${first}", Path: "${second}"}.pb()}
 	testRule2 := Rule{regexp.MustCompile(`x/(?P<sig>\w+)/y/(?P<tail>.+)$`), V{Path: "${tail}", Sig: "|${sig}|"}.pb()}