reflect/protodesc: enforce strict validation

Hyrum's Law dictates that if we do not prevent naughty behavior,
people will rely on it. If we do not validate that the provided
file descriptor is correct today, it will be near impossible
to add proper validation checks later on.

The logic added validates that the provided file descriptor is
correct according to the same semantics as protoc,
which was reversed engineered to derive the set of rules implemented here.
The rules are unfortunately complicated because protobuf is a language
full of many non-orthogonal features. While our logic is complicated,
it is still 1/7th the size of the equivalent C++ code!

Change-Id: I6acc5dc3bd2e4c6bea6cd9e81214f8104402602a
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/184837
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/reflect/protodesc/file_test.go b/reflect/protodesc/file_test.go
index 45910fc..26bc174 100644
--- a/reflect/protodesc/file_test.go
+++ b/reflect/protodesc/file_test.go
@@ -11,6 +11,7 @@
 
 	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/reflect/protoregistry"
 
 	"google.golang.org/protobuf/types/descriptorpb"
@@ -30,6 +31,60 @@
 	return out
 }
 
+var (
+	proto2Enum = mustParseFile(`
+		syntax:    "proto2"
+		name:      "proto2_enum.proto"
+		package:   "test.proto2"
+		enum_type: [{name:"Enum" value:[{name:"ONE" number:1}]}]
+	`)
+	proto3Message = mustParseFile(`
+		syntax:    "proto3"
+		name:      "proto3_message.proto"
+		package:   "test.proto3"
+		message_type: [{
+			name:  "Message"
+			field: [
+				{name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING},
+				{name:"bar" number:2 label:LABEL_OPTIONAL type:TYPE_STRING}
+			]
+		}]
+	`)
+	extendableMessage = mustParseFile(`
+		syntax:       "proto2"
+		name:         "extendable_message.proto"
+		package:      "test.proto2"
+		message_type: [{name:"Message" extension_range:[{start:1 end:1000}]}]
+	`)
+	importPublicFile1 = mustParseFile(`
+		syntax:            "proto3"
+		name:              "import_public1.proto"
+		dependency:        ["proto2_enum.proto", "proto3_message.proto", "extendable_message.proto"]
+		message_type:      [{name:"Public1"}]
+	`)
+	importPublicFile2 = mustParseFile(`
+		syntax:            "proto3"
+		name:              "import_public2.proto"
+		dependency:        ["import_public1.proto"]
+		public_dependency: [0]
+		message_type:      [{name:"Public2"}]
+	`)
+	importPublicFile3 = mustParseFile(`
+		syntax:            "proto3"
+		name:              "import_public3.proto"
+		dependency:        ["import_public2.proto", "extendable_message.proto"]
+		public_dependency: [0]
+		message_type:      [{name:"Public3"}]
+	`)
+	importPublicFile4 = mustParseFile(`
+		syntax:            "proto3"
+		name:              "import_public4.proto"
+		dependency:        ["import_public2.proto", "import_public3.proto", "proto2_enum.proto"]
+		public_dependency: [0, 1]
+		message_type:      [{name:"Public4"}]
+	`)
+)
+
 func TestNewFile(t *testing.T) {
 	tests := []struct {
 		label    string
@@ -39,6 +94,94 @@
 		wantDesc *descriptorpb.FileDescriptorProto
 		wantErr  string
 	}{{
+		label:   "empty path",
+		inDesc:  mustParseFile(``),
+		wantErr: `path must be populated`,
+	}, {
+		label:  "empty package and syntax",
+		inDesc: mustParseFile(`name:"weird" package:""`),
+	}, {
+		label:   "invalid syntax",
+		inDesc:  mustParseFile(`name:"weird" syntax:"proto9"`),
+		wantErr: `invalid syntax: "proto9"`,
+	}, {
+		label:   "bad package",
+		inDesc:  mustParseFile(`name:"weird" package:"$"`),
+		wantErr: `invalid package: "$"`,
+	}, {
+		label: "unresolvable import",
+		inDesc: mustParseFile(`
+			name:       "test.proto"
+			package:    ""
+			dependency: "dep.proto"
+		`),
+		wantErr: `could not resolve import "dep.proto": not found`,
+	}, {
+		label: "unresolvable import but allowed",
+		inDesc: mustParseFile(`
+			name:       "test.proto"
+			package:    ""
+			dependency: "dep.proto"
+		`),
+		inOpts: []option{allowUnresolvable()},
+	}, {
+		label: "duplicate import",
+		inDesc: mustParseFile(`
+			name:       "test.proto"
+			package:    ""
+			dependency: ["dep.proto", "dep.proto"]
+		`),
+		inOpts:  []option{allowUnresolvable()},
+		wantErr: `already imported "dep.proto"`,
+	}, {
+		label: "invalid weak import",
+		inDesc: mustParseFile(`
+			name:            "test.proto"
+			package:         ""
+			dependency:      "dep.proto"
+			weak_dependency: [-23]
+		`),
+		inOpts:  []option{allowUnresolvable()},
+		wantErr: `invalid or duplicate weak import index: -23`,
+	}, {
+		label: "normal weak and public import",
+		inDesc: mustParseFile(`
+			name:              "test.proto"
+			package:           ""
+			dependency:        "dep.proto"
+			weak_dependency:   [0]
+			public_dependency: [0]
+		`),
+		inOpts: []option{allowUnresolvable()},
+	}, {
+		label: "import public indirect dependency duplicate",
+		inDeps: []*descriptorpb.FileDescriptorProto{
+			mustParseFile(`name:"leaf.proto"`),
+			mustParseFile(`name:"public.proto" dependency:"leaf.proto" public_dependency:0`),
+		},
+		inDesc: mustParseFile(`
+			name: "test.proto"
+			package: ""
+			dependency: ["public.proto", "leaf.proto"]
+		`),
+	}, {
+		label: "import public graph",
+		inDeps: []*descriptorpb.FileDescriptorProto{
+			cloneFile(proto2Enum),
+			cloneFile(proto3Message),
+			cloneFile(extendableMessage),
+			cloneFile(importPublicFile1),
+			cloneFile(importPublicFile2),
+			cloneFile(importPublicFile3),
+			cloneFile(importPublicFile4),
+		},
+		inDesc: mustParseFile(`
+			name:       "test.proto"
+			package:    "test.graph"
+			dependency: ["import_public4.proto"],
+		`),
+		// TODO: Test import public
+	}, {
 		label: "resolve relative reference",
 		inDesc: mustParseFile(`
 			name: "test.proto"
@@ -216,6 +359,499 @@
 				field: [{name:"F" number:1 label:LABEL_OPTIONAL type:TYPE_MESSAGE type_name:".fizz.M.M"}]
 			}]
 		`),
+	}, {
+		label: "namespace conflict on enum value",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			enum_type: [{
+				name: "foo"
+				value: [{name:"foo" number:0}]
+			}]
+		`),
+		wantErr: `descriptor "foo" already declared`,
+	}, {
+		label: "no namespace conflict on message field",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{
+				name: "foo"
+				field: [{name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]
+		`),
+	}, {
+		label: "invalid name",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name: "$"}]
+		`),
+		wantErr: `descriptor "" has an invalid nested name: "$"`,
+	}, {
+		label: "invalid empty enum",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{name:"E"}]}]
+		`),
+		wantErr: `enum "M.E" must contain at least one value declaration`,
+	}, {
+		label: "invalid enum value without number",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{name:"E" value:[{name:"one"}]}]}]
+		`),
+		wantErr: `enum value "M.one" must have a specified number`,
+	}, {
+		label: "valid enum",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{name:"E" value:[{name:"one" number:1}]}]}]
+		`),
+	}, {
+		label: "invalid enum reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:          "E"
+				reserved_name: [""]
+				value: [{name:"V" number:0}]
+			}]}]
+		`),
+		// NOTE: In theory this should be an error.
+		// See https://github.com/protocolbuffers/protobuf/issues/6335.
+		/*wantErr: `enum "M.E" reserved names has invalid name: ""`,*/
+	}, {
+		label: "duplicate enum reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:          "E"
+				reserved_name: ["foo", "foo"]
+			}]}]
+		`),
+		wantErr: `enum "M.E" reserved names has duplicate name: "foo"`,
+	}, {
+		label: "valid enum reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:          "E"
+				reserved_name: ["foo", "bar"]
+				value:         [{name:"baz" number:1}]
+			}]}]
+		`),
+	}, {
+		label: "use of enum reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:          "E"
+				reserved_name: ["foo", "bar"]
+				value:         [{name:"foo" number:1}]
+			}]}]
+		`),
+		wantErr: `enum value "M.foo" must not use reserved name`,
+	}, {
+		label: "invalid enum reserved ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:           "E"
+				reserved_range: [{start:5 end:4}]
+			}]}]
+		`),
+		wantErr: `enum "M.E" reserved ranges has invalid range: 5 to 4`,
+	}, {
+		label: "overlapping enum reserved ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:           "E"
+				reserved_range: [{start:1 end:1000}, {start:10 end:100}]
+			}]}]
+		`),
+		wantErr: `enum "M.E" reserved ranges has overlapping ranges: 1 to 1000 with 10 to 100`,
+	}, {
+		label: "valid enum reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:           "E"
+				reserved_range: [{start:1 end:10}, {start:100 end:1000}]
+				value:          [{name:"baz" number:50}]
+			}]}]
+		`),
+	}, {
+		label: "use of enum reserved range",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:           "E"
+				reserved_range: [{start:1 end:10}, {start:100 end:1000}]
+				value:          [{name:"baz" number:500}]
+			}]}]
+		`),
+		wantErr: `enum value "M.baz" must not use reserved number 500`,
+	}, {
+		label: "unused enum alias feature",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:    "E"
+				value:   [{name:"baz" number:500}]
+				options: {allow_alias:true}
+			}]}]
+		`),
+		wantErr: `enum "M.E" allows aliases, but none were found`,
+	}, {
+		label: "enum number conflicts",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"foo" number:0}, {name:"bar" number:1}, {name:"baz" number:1}]
+			}]}]
+		`),
+		wantErr: `enum "M.E" has conflicting non-aliased values on number 1: "baz" with "bar"`,
+	}, {
+		label: "aliased enum numbers",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:    "E"
+				value:   [{name:"foo" number:0}, {name:"bar" number:1}, {name:"baz" number:1}]
+				options: {allow_alias:true}
+			}]}]
+		`),
+	}, {
+		label: "invalid proto3 enum",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"baz" number:500}]
+			}]}]
+		`),
+		wantErr: `enum "M.baz" using proto3 semantics must have zero number for the first value`,
+	}, {
+		label: "valid proto3 enum",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"baz" number:0}]
+			}]}]
+		`),
+	}, {
+		label: "proto3 enum name prefix conflict",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"e_Foo" number:0}, {name:"fOo" number:1}]
+			}]}]
+		`),
+		wantErr: `enum "M.E" using proto3 semantics has conflict: "fOo" with "e_Foo"`,
+	}, {
+		label: "proto2 enum has name prefix check",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"e_Foo" number:0}, {name:"fOo" number:1}]
+			}]}]
+		`),
+	}, {
+		label: "proto3 enum same name prefix with number conflict",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:  "E"
+				value: [{name:"e_Foo" number:0}, {name:"fOo" number:0}]
+			}]}]
+		`),
+		wantErr: `enum "M.E" has conflicting non-aliased values on number 0: "fOo" with "e_Foo"`,
+	}, {
+		label: "proto3 enum same name prefix with alias numbers",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" enum_type:[{
+				name:    "E"
+				value:   [{name:"e_Foo" number:0}, {name:"fOo" number:0}]
+				options: {allow_alias: true}
+			}]}]
+		`),
+	}, {
+		label: "invalid message reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:          "M"
+				reserved_name: ["$"]
+			}]}]
+		`),
+		// NOTE: In theory this should be an error.
+		// See https://github.com/protocolbuffers/protobuf/issues/6335.
+		/*wantErr: `message "M.M" reserved names has invalid name: "$"`,*/
+	}, {
+		label: "valid message reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:          "M"
+				reserved_name: ["foo", "bar"]
+				field:         [{name:"foo" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message field "M.M.foo" must not use reserved name`,
+	}, {
+		label: "valid message reserved names",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:          "M"
+				reserved_name: ["foo", "bar"]
+				field:         [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}]
+				oneof_decl:    [{name:"foo"}] # not affected by reserved_name
+			}]}]
+		`),
+	}, {
+		label: "invalid reserved number",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:           "M"
+				reserved_range: [{start:1 end:1}]
+				field:          [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message "M.M" reserved ranges has invalid field number: 0`,
+	}, {
+		label: "invalid reserved ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:           "M"
+				reserved_range: [{start:2 end:2}]
+				field:          [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message "M.M" reserved ranges has invalid range: 2 to 1`,
+	}, {
+		label: "overlapping reserved ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:           "M"
+				reserved_range: [{start:1 end:10}, {start:2 end:9}]
+				field:          [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message "M.M" reserved ranges has overlapping ranges: 1 to 9 with 2 to 8`,
+	}, {
+		label: "use of reserved message field number",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:           "M"
+				reserved_range: [{start:10 end:20}, {start:20 end:30}, {start:30 end:31}]
+				field:          [{name:"baz" number:30 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message field "M.M.baz" must not use reserved number 30`,
+	}, {
+		label: "invalid extension ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:            "M"
+				extension_range: [{start:-500 end:2}]
+				field:           [{name:"baz" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}]
+			}]}]
+		`),
+		wantErr: `message "M.M" extension ranges has invalid field number: -500`,
+	}, {
+		label: "overlapping reserved and extension ranges",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:            "M"
+				reserved_range:  [{start:15 end:20}, {start:1 end:3}, {start:7 end:10}]
+				extension_range: [{start:8 end:9}, {start:3 end:5}]
+			}]}]
+		`),
+		wantErr: `message "M.M" reserved and extension ranges has overlapping ranges: 7 to 9 with 8`,
+	}, {
+		label: "message field conflicting number",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:            "M"
+				field: [
+					{name:"one" number:1 label:LABEL_OPTIONAL type:TYPE_STRING},
+					{name:"One" number:1 label:LABEL_OPTIONAL type:TYPE_STRING}
+				]
+			}]}]
+		`),
+		wantErr: `message "M.M" has conflicting fields: "One" with "one"`,
+	}, {
+		label: "invalid MessageSet",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:    "M"
+				options: {message_set_wire_format:true}
+			}]}]
+		`),
+		wantErr: `message "M.M" is an invalid proto1 MessageSet`,
+	}, {
+		label: "valid MessageSet",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:            "M"
+				extension_range: [{start:1 end:100000}]
+				options:         {message_set_wire_format:true}
+			}]}]
+		`),
+	}, {
+		label: "invalid extension ranges in proto3",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:            "M"
+				extension_range: [{start:1 end:100000}]
+			}]}]
+		`),
+		wantErr: `message "M.M" using proto3 semantics cannot have extension ranges`,
+	}, {
+		label: "proto3 message fields conflict",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name: "M"
+				field: [
+					{name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING},
+					{name:"baz" number:2 label:LABEL_OPTIONAL type:TYPE_STRING}
+				]
+			}]}]
+		`),
+		wantErr: `proto: message "M.M" using proto3 semantics has conflict: "baz" with "_b_a_z_"`,
+	}, {
+		label: "proto3 message fields",
+		inDesc: mustParseFile(`
+			syntax:  "proto3"
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name:       "M"
+				field:      [{name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0}]
+				oneof_decl: [{name:"baz"}] # proto3 name conflict logic does not include oneof
+			}]}]
+		`),
+	}, {
+		label: "proto2 message fields with no conflict",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			message_type: [{name:"M" nested_type:[{
+				name: "M"
+				field: [
+					{name:"_b_a_z_" number:1 label:LABEL_OPTIONAL type:TYPE_STRING},
+					{name:"baz" number:2 label:LABEL_OPTIONAL type:TYPE_STRING}
+				]
+			}]}]
+		`),
+		// TODO: Test field and oneof handling in validateMessageDeclarations
+		// TODO: Test unmarshalDefault
+		// TODO: Test validateExtensionDeclarations
+		// TODO: Test checkValidGroup
+		// TODO: Test checkValidMap
+	}, {
+		label: "empty service",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			service: [{name:"service"}]
+		`),
+	}, {
+		label: "service with method with unresolved",
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			service: [{
+				name: "service"
+				method: [{
+					name:"method"
+					input_type:"foo"
+					output_type:".foo.bar.baz"
+				}]
+			}]
+		`),
+		inOpts: []option{allowUnresolvable()},
+	}, {
+		label: "service with wrong reference type",
+		inDeps: []*descriptorpb.FileDescriptorProto{
+			cloneFile(proto3Message),
+			cloneFile(proto2Enum),
+		},
+		inDesc: mustParseFile(`
+			name:    "test.proto"
+			package: ""
+			dependency: ["proto2_enum.proto", "proto3_message.proto"]
+			service: [{
+				name: "service"
+				method: [{
+					name:        "method"
+					input_type:  ".test.proto2.Enum",
+					output_type: ".test.proto3.Message"
+				}]
+			}]
+		`),
+		wantErr: `service method "service.method" cannot resolve input: resolved "test.proto2.Enum", but it is not an message`,
 	}}
 
 	for _, tt := range tests {
@@ -247,3 +883,57 @@
 		})
 	}
 }
+
+func TestName(t *testing.T) {
+	tests := []struct {
+		in            protoreflect.Name
+		enumPrefix    string
+		wantMapEntry  protoreflect.Name
+		wantEnumValue string
+		wantTrimValue protoreflect.Name
+		wantJSON      string
+	}{{
+		in:            "abc",
+		enumPrefix:    "",
+		wantMapEntry:  "AbcEntry",
+		wantEnumValue: "Abc",
+		wantTrimValue: "abc",
+		wantJSON:      "abc",
+	}, {
+		in:            "foo_baR_",
+		enumPrefix:    "foo_bar",
+		wantMapEntry:  "FooBaREntry",
+		wantEnumValue: "FooBar",
+		wantTrimValue: "foo_baR_",
+		wantJSON:      "fooBaR",
+	}, {
+		in:            "snake_caseCamelCase",
+		enumPrefix:    "snakecasecamel",
+		wantMapEntry:  "SnakeCaseCamelCaseEntry",
+		wantEnumValue: "SnakeCasecamelcase",
+		wantTrimValue: "Case",
+		wantJSON:      "snakeCaseCamelCase",
+	}, {
+		in:            "FiZz_BuZz",
+		enumPrefix:    "fizz",
+		wantMapEntry:  "FiZzBuZzEntry",
+		wantEnumValue: "FizzBuzz",
+		wantTrimValue: "BuZz",
+		wantJSON:      "FiZzBuZz",
+	}}
+
+	for _, tt := range tests {
+		if got := mapEntryName(tt.in); got != tt.wantMapEntry {
+			t.Errorf("mapEntryName(%q) = %q, want %q", tt.in, got, tt.wantMapEntry)
+		}
+		if got := enumValueName(tt.in); got != tt.wantEnumValue {
+			t.Errorf("enumValueName(%q) = %q, want %q", tt.in, got, tt.wantEnumValue)
+		}
+		if got := trimEnumPrefix(tt.in, tt.enumPrefix); got != tt.wantTrimValue {
+			t.Errorf("trimEnumPrefix(%q, %q) = %q, want %q", tt.in, tt.enumPrefix, got, tt.wantTrimValue)
+		}
+		if got := jsonName(tt.in); got != tt.wantJSON {
+			t.Errorf("jsonName(%q) = %q, want %q", tt.in, got, tt.wantJSON)
+		}
+	}
+}