github.com/jhump/protoreflect@v1.16.0/desc/protoparse/parser_test.go (about) 1 package protoparse 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 "testing" 14 15 "github.com/bufbuild/protocompile/parser" 16 "github.com/bufbuild/protocompile/reporter" 17 "github.com/golang/protobuf/proto" 18 "google.golang.org/protobuf/types/descriptorpb" 19 20 "github.com/jhump/protoreflect/desc" 21 "github.com/jhump/protoreflect/internal/testprotos" 22 "github.com/jhump/protoreflect/internal/testutil" 23 ) 24 25 func TestEmptyParse(t *testing.T) { 26 p := Parser{ 27 Accessor: func(filename string) (io.ReadCloser, error) { 28 return ioutil.NopCloser(bytes.NewReader(nil)), nil 29 }, 30 } 31 fd, err := p.ParseFiles("foo.proto") 32 testutil.Ok(t, err) 33 testutil.Eq(t, 1, len(fd)) 34 testutil.Eq(t, "foo.proto", fd[0].GetName()) 35 testutil.Eq(t, 0, len(fd[0].GetDependencies())) 36 testutil.Eq(t, 0, len(fd[0].GetMessageTypes())) 37 testutil.Eq(t, 0, len(fd[0].GetEnumTypes())) 38 testutil.Eq(t, 0, len(fd[0].GetExtensions())) 39 testutil.Eq(t, 0, len(fd[0].GetServices())) 40 } 41 42 func TestJunkParse(t *testing.T) { 43 // inputs that have been found in the past to cause panics by oss-fuzz 44 inputs := map[string]string{ 45 "case-34232": `'';`, 46 "case-34238": `.`, 47 } 48 for name, input := range inputs { 49 protoName := fmt.Sprintf("%s.proto", name) 50 p := Parser{ 51 Accessor: FileContentsFromMap(map[string]string{protoName: input}), 52 } 53 _, err := p.ParseFiles(protoName) 54 // we expect this to error... but we don't want it to panic 55 testutil.Nok(t, err, "junk input should have returned error") 56 t.Logf("error from parse: %v", err) 57 } 58 } 59 60 func TestSimpleParse(t *testing.T) { 61 protos := map[string]parser.Result{} 62 63 // Just verify that we can successfully parse the same files we use for 64 // testing. We do a *very* shallow check of what was parsed because we know 65 // it won't be fully correct until after linking. (So that will be tested 66 // below, where we parse *and* link.) 67 res, err := parseFileForTest("../../internal/testprotos/desc_test1.proto") 68 testutil.Ok(t, err) 69 fd := res.FileDescriptorProto() 70 testutil.Eq(t, "../../internal/testprotos/desc_test1.proto", fd.GetName()) 71 testutil.Eq(t, "testprotos", fd.GetPackage()) 72 testutil.Require(t, hasExtension(fd, "xtm")) 73 testutil.Require(t, hasMessage(fd, "TestMessage")) 74 protos[fd.GetName()] = res 75 76 res, err = parseFileForTest("../../internal/testprotos/desc_test2.proto") 77 testutil.Ok(t, err) 78 fd = res.FileDescriptorProto() 79 testutil.Eq(t, "../../internal/testprotos/desc_test2.proto", fd.GetName()) 80 testutil.Eq(t, "testprotos", fd.GetPackage()) 81 testutil.Require(t, hasExtension(fd, "groupx")) 82 testutil.Require(t, hasMessage(fd, "GroupX")) 83 testutil.Require(t, hasMessage(fd, "Frobnitz")) 84 protos[fd.GetName()] = res 85 86 res, err = parseFileForTest("../../internal/testprotos/desc_test_defaults.proto") 87 testutil.Ok(t, err) 88 fd = res.FileDescriptorProto() 89 testutil.Eq(t, "../../internal/testprotos/desc_test_defaults.proto", fd.GetName()) 90 testutil.Eq(t, "testprotos", fd.GetPackage()) 91 testutil.Require(t, hasMessage(fd, "PrimitiveDefaults")) 92 protos[fd.GetName()] = res 93 94 res, err = parseFileForTest("../../internal/testprotos/desc_test_field_types.proto") 95 testutil.Ok(t, err) 96 fd = res.FileDescriptorProto() 97 testutil.Eq(t, "../../internal/testprotos/desc_test_field_types.proto", fd.GetName()) 98 testutil.Eq(t, "testprotos", fd.GetPackage()) 99 testutil.Require(t, hasEnum(fd, "TestEnum")) 100 testutil.Require(t, hasMessage(fd, "UnaryFields")) 101 protos[fd.GetName()] = res 102 103 res, err = parseFileForTest("../../internal/testprotos/desc_test_options.proto") 104 testutil.Ok(t, err) 105 fd = res.FileDescriptorProto() 106 testutil.Eq(t, "../../internal/testprotos/desc_test_options.proto", fd.GetName()) 107 testutil.Eq(t, "testprotos", fd.GetPackage()) 108 testutil.Require(t, hasExtension(fd, "mfubar")) 109 testutil.Require(t, hasEnum(fd, "ReallySimpleEnum")) 110 testutil.Require(t, hasMessage(fd, "ReallySimpleMessage")) 111 protos[fd.GetName()] = res 112 113 res, err = parseFileForTest("../../internal/testprotos/desc_test_proto3.proto") 114 testutil.Ok(t, err) 115 fd = res.FileDescriptorProto() 116 testutil.Eq(t, "../../internal/testprotos/desc_test_proto3.proto", fd.GetName()) 117 testutil.Eq(t, "testprotos", fd.GetPackage()) 118 testutil.Require(t, hasEnum(fd, "Proto3Enum")) 119 testutil.Require(t, hasService(fd, "TestService")) 120 protos[fd.GetName()] = res 121 122 res, err = parseFileForTest("../../internal/testprotos/desc_test_wellknowntypes.proto") 123 testutil.Ok(t, err) 124 fd = res.FileDescriptorProto() 125 testutil.Eq(t, "../../internal/testprotos/desc_test_wellknowntypes.proto", fd.GetName()) 126 testutil.Eq(t, "testprotos", fd.GetPackage()) 127 testutil.Require(t, hasMessage(fd, "TestWellKnownTypes")) 128 protos[fd.GetName()] = res 129 130 res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg.proto") 131 testutil.Ok(t, err) 132 fd = res.FileDescriptorProto() 133 testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg.proto", fd.GetName()) 134 testutil.Eq(t, "", fd.GetPackage()) 135 protos[fd.GetName()] = res 136 137 res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg_new.proto") 138 testutil.Ok(t, err) 139 fd = res.FileDescriptorProto() 140 testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg_new.proto", fd.GetName()) 141 testutil.Eq(t, "", fd.GetPackage()) 142 testutil.Require(t, hasMessage(fd, "TopLevel")) 143 protos[fd.GetName()] = res 144 145 res, err = parseFileForTest("../../internal/testprotos/pkg/desc_test_pkg.proto") 146 testutil.Ok(t, err) 147 fd = res.FileDescriptorProto() 148 testutil.Eq(t, "../../internal/testprotos/pkg/desc_test_pkg.proto", fd.GetName()) 149 testutil.Eq(t, "jhump.protoreflect.desc", fd.GetPackage()) 150 testutil.Require(t, hasEnum(fd, "Foo")) 151 testutil.Require(t, hasMessage(fd, "Bar")) 152 protos[fd.GetName()] = res 153 154 // We'll also check our fixup logic to make sure it correctly rewrites the 155 // names of the files to match corresponding import statementes. This should 156 // strip the "../../internal/testprotos/" prefix from each file. 157 protos, _ = fixupFilenames(protos) 158 var actual []string 159 for n := range protos { 160 actual = append(actual, n) 161 } 162 sort.Strings(actual) 163 expected := []string{ 164 "desc_test1.proto", 165 "desc_test2.proto", 166 "desc_test_defaults.proto", 167 "desc_test_field_types.proto", 168 "desc_test_options.proto", 169 "desc_test_proto3.proto", 170 "desc_test_wellknowntypes.proto", 171 "nopkg/desc_test_nopkg.proto", 172 "nopkg/desc_test_nopkg_new.proto", 173 "pkg/desc_test_pkg.proto", 174 } 175 testutil.Eq(t, expected, actual) 176 } 177 178 func parseFileForTest(filename string) (parser.Result, error) { 179 filenames := []string{filename} 180 res, _ := Parser{}.getResolver(filenames) 181 rep := reporter.NewHandler(nil) 182 results, err := parseToProtos(res, filenames, rep, true) 183 if err != nil { 184 return nil, err 185 } 186 return results[0], nil 187 } 188 189 func hasExtension(fd *descriptorpb.FileDescriptorProto, name string) bool { 190 for _, ext := range fd.Extension { 191 if ext.GetName() == name { 192 return true 193 } 194 } 195 return false 196 } 197 198 func hasMessage(fd *descriptorpb.FileDescriptorProto, name string) bool { 199 for _, md := range fd.MessageType { 200 if md.GetName() == name { 201 return true 202 } 203 } 204 return false 205 } 206 207 func hasEnum(fd *descriptorpb.FileDescriptorProto, name string) bool { 208 for _, ed := range fd.EnumType { 209 if ed.GetName() == name { 210 return true 211 } 212 } 213 return false 214 } 215 216 func hasService(fd *descriptorpb.FileDescriptorProto, name string) bool { 217 for _, sd := range fd.Service { 218 if sd.GetName() == name { 219 return true 220 } 221 } 222 return false 223 } 224 225 func TestAggregateValueInUninterpretedOptions(t *testing.T) { 226 res, err := parseFileForTest("../../internal/testprotos/desc_test_complex.proto") 227 testutil.Ok(t, err) 228 fd := res.FileDescriptorProto() 229 230 // service TestTestService, method UserAuth; first option 231 aggregateValue1 := *fd.Service[0].Method[0].Options.UninterpretedOption[0].AggregateValue 232 testutil.Eq(t, `authenticated : true permission : { action : LOGIN entity : "client" }`, aggregateValue1) 233 234 // service TestTestService, method Get; first option 235 aggregateValue2 := *fd.Service[0].Method[1].Options.UninterpretedOption[0].AggregateValue 236 testutil.Eq(t, `authenticated : true permission : { action : READ entity : "user" }`, aggregateValue2) 237 238 // message Another; first option 239 aggregateValue3 := *fd.MessageType[4].Options.UninterpretedOption[0].AggregateValue 240 testutil.Eq(t, `foo : "abc" s < name : "foo" , id : 123 > , array : [ 1 , 2 , 3 ] , r : [ < name : "f" > , { name : "s" } , { id : 456 } ] ,`, aggregateValue3) 241 242 // message Test.Nested._NestedNested; second option (rept) 243 // (Test.Nested is at index 1 instead of 0 because of implicit nested message from map field m) 244 aggregateValue4 := *fd.MessageType[1].NestedType[1].NestedType[0].Options.UninterpretedOption[1].AggregateValue 245 testutil.Eq(t, `foo : "goo" [ foo . bar . Test . Nested . _NestedNested . _garblez ] : "boo"`, aggregateValue4) 246 } 247 248 func TestParseFilesMessageComments(t *testing.T) { 249 p := Parser{ 250 IncludeSourceCodeInfo: true, 251 } 252 protos, err := p.ParseFiles("../../internal/testprotos/desc_test1.proto") 253 testutil.Ok(t, err) 254 comments := "" 255 expected := " Comment for TestMessage\n" 256 for _, p := range protos { 257 msg := p.FindMessage("testprotos.TestMessage") 258 if msg != nil { 259 si := msg.GetSourceInfo() 260 if si != nil { 261 comments = si.GetLeadingComments() 262 } 263 break 264 } 265 } 266 testutil.Eq(t, expected, comments) 267 } 268 269 func TestParseFilesWithImportsNoImportPath(t *testing.T) { 270 relFilePaths := []string{ 271 "a/b/b1.proto", 272 "a/b/b2.proto", 273 "c/c.proto", 274 } 275 276 pwd, err := os.Getwd() 277 testutil.Require(t, err == nil, "%v", err) 278 279 err = os.Chdir("../../internal/testprotos/protoparse") 280 testutil.Require(t, err == nil, "%v", err) 281 p := Parser{} 282 protos, parseErr := p.ParseFiles(relFilePaths...) 283 err = os.Chdir(pwd) 284 testutil.Require(t, err == nil, "%v", err) 285 testutil.Require(t, parseErr == nil, "%v", parseErr) 286 287 testutil.Ok(t, err) 288 testutil.Eq(t, len(relFilePaths), len(protos)) 289 } 290 291 func TestParseFilesWithDependencies(t *testing.T) { 292 // Create some file contents that import a non-well-known proto. 293 // (One of the protos in internal/testprotos is fine.) 294 contents := map[string]string{ 295 "test.proto": ` 296 syntax = "proto3"; 297 import "desc_test_wellknowntypes.proto"; 298 299 message TestImportedType { 300 testprotos.TestWellKnownTypes imported_field = 1; 301 } 302 `, 303 } 304 305 // Establish that we *can* parse the source file with a parser that 306 // registers the dependency. 307 t.Run("DependencyIncluded", func(t *testing.T) { 308 // Create a dependency-aware parser. 309 parser := Parser{ 310 Accessor: FileContentsFromMap(contents), 311 LookupImport: func(imp string) (*desc.FileDescriptor, error) { 312 if imp == "desc_test_wellknowntypes.proto" { 313 return desc.LoadFileDescriptor(imp) 314 } 315 return nil, errors.New("unexpected filename") 316 }, 317 } 318 if _, err := parser.ParseFiles("test.proto"); err != nil { 319 t.Errorf("Could not parse with a non-well-known import: %v", err) 320 } 321 }) 322 t.Run("DependencyIncludedProto", func(t *testing.T) { 323 // Create a dependency-aware parser. 324 parser := Parser{ 325 Accessor: FileContentsFromMap(contents), 326 LookupImportProto: func(imp string) (*descriptorpb.FileDescriptorProto, error) { 327 if imp == "desc_test_wellknowntypes.proto" { 328 fileDescriptor, err := desc.LoadFileDescriptor(imp) 329 if err != nil { 330 return nil, err 331 } 332 return fileDescriptor.AsFileDescriptorProto(), nil 333 } 334 return nil, errors.New("unexpected filename") 335 }, 336 } 337 if _, err := parser.ParseFiles("test.proto"); err != nil { 338 t.Errorf("Could not parse with a non-well-known import: %v", err) 339 } 340 }) 341 342 // Establish that we *can not* parse the source file with a parser that 343 // did not register the dependency. 344 t.Run("DependencyExcluded", func(t *testing.T) { 345 // Create a dependency-aware parser. 346 parser := Parser{ 347 Accessor: FileContentsFromMap(contents), 348 } 349 if _, err := parser.ParseFiles("test.proto"); err == nil { 350 t.Errorf("Expected parse to fail due to lack of an import.") 351 } 352 }) 353 354 // Establish that the accessor has precedence over LookupImport. 355 t.Run("AccessorWins", func(t *testing.T) { 356 // Create a dependency-aware parser that should never be called. 357 parser := Parser{ 358 Accessor: FileContentsFromMap(map[string]string{ 359 "test.proto": `syntax = "proto3";`, 360 }), 361 LookupImport: func(imp string) (*desc.FileDescriptor, error) { 362 // It's okay for descriptor.proto to be requested implicitly, but 363 // nothing else should make it here since it should instead be 364 // retrieved via Accessor. 365 if imp != "google/protobuf/descriptor.proto" { 366 t.Errorf("LookupImport was called on a filename available to the Accessor: %q", imp) 367 } 368 return nil, errors.New("unimportant") 369 }, 370 } 371 if _, err := parser.ParseFiles("test.proto"); err != nil { 372 t.Error(err) 373 } 374 }) 375 } 376 377 func TestParseFilesReturnsKnownExtensions(t *testing.T) { 378 accessor := func(filename string) (io.ReadCloser, error) { 379 if filename == "desc_test3.proto" { 380 return io.NopCloser(strings.NewReader(` 381 syntax = "proto3"; 382 import "desc_test_complex.proto"; 383 message Foo { 384 foo.bar.Simple field = 1; 385 } 386 `)), nil 387 } 388 return os.Open(filepath.Join("../../internal/testprotos", filename)) 389 } 390 p := Parser{ 391 Accessor: accessor, 392 } 393 fds, err := p.ParseFiles("desc_test3.proto") 394 testutil.Ok(t, err) 395 fd := fds[0].GetDependencies()[0] 396 md := fd.FindMessage("foo.bar.Test.Nested._NestedNested") 397 testutil.Require(t, md != nil) 398 val, err := proto.GetExtension(md.GetOptions(), testprotos.E_Rept) 399 testutil.Ok(t, err) 400 _, ok := val.([]*testprotos.Test) 401 testutil.Require(t, ok) 402 } 403 404 func TestParseCommentsBeforeDot(t *testing.T) { 405 accessor := FileContentsFromMap(map[string]string{ 406 "test.proto": ` 407 syntax = "proto3"; 408 message Foo { 409 // leading comments 410 .Foo foo = 1; 411 } 412 `, 413 }) 414 415 p := Parser{ 416 Accessor: accessor, 417 IncludeSourceCodeInfo: true, 418 } 419 fds, err := p.ParseFiles("test.proto") 420 testutil.Ok(t, err) 421 422 comment := fds[0].GetMessageTypes()[0].GetFields()[0].GetSourceInfo().GetLeadingComments() 423 testutil.Eq(t, " leading comments\n", comment) 424 } 425 426 func TestParseInferImportPaths_SimpleNoOp(t *testing.T) { 427 sources := map[string]string{ 428 "test.proto": ` 429 syntax = "proto3"; 430 import "google/protobuf/struct.proto"; 431 message Foo { 432 string name = 1; 433 repeated uint64 refs = 2; 434 google.protobuf.Struct meta = 3; 435 }`, 436 } 437 p := Parser{ 438 Accessor: FileContentsFromMap(sources), 439 InferImportPaths: true, 440 } 441 fds, err := p.ParseFiles("test.proto") 442 testutil.Ok(t, err) 443 testutil.Eq(t, 1, len(fds)) 444 } 445 446 func TestParseInferImportPaths_FixesNestedPaths(t *testing.T) { 447 sources := FileContentsFromMap(map[string]string{ 448 "/foo/bar/a.proto": ` 449 syntax = "proto3"; 450 import "baz/b.proto"; 451 message A { 452 B b = 1; 453 }`, 454 "/foo/bar/baz/b.proto": ` 455 syntax = "proto3"; 456 import "baz/c.proto"; 457 message B { 458 C c = 1; 459 }`, 460 "/foo/bar/baz/c.proto": ` 461 syntax = "proto3"; 462 message C {}`, 463 "/foo/bar/baz/d.proto": ` 464 syntax = "proto3"; 465 import "a.proto"; 466 message D { 467 A a = 1; 468 }`, 469 }) 470 471 testCases := []struct { 472 name string 473 cwd string 474 filenames []string 475 expect []string 476 }{ 477 { 478 name: "outside hierarchy", 479 cwd: "/buzz", 480 filenames: []string{"../foo/bar/a.proto", "../foo/bar/baz/b.proto", "../foo/bar/baz/c.proto", "../foo/bar/baz/d.proto"}, 481 }, 482 { 483 name: "inside hierarchy", 484 cwd: "/foo", 485 filenames: []string{"bar/a.proto", "bar/baz/b.proto", "bar/baz/c.proto", "bar/baz/d.proto"}, 486 }, 487 { 488 name: "import path root (no-op)", 489 cwd: "/foo/bar", 490 filenames: []string{"a.proto", "baz/b.proto", "baz/c.proto", "baz/d.proto"}, 491 }, 492 { 493 name: "inside leaf directory", 494 cwd: "/foo/bar/baz", 495 filenames: []string{"../a.proto", "b.proto", "c.proto", "d.proto"}, 496 // NB: Expected names differ from above cases because nothing imports d.proto. 497 // So when inferring the root paths, the fact that d.proto is defined in 498 // the baz sub-directory will not be discovered. That's okay since the parse 499 // operation still succeeds. 500 expect: []string{"a.proto", "baz/b.proto", "baz/c.proto", "d.proto"}, 501 }, 502 } 503 for _, testCase := range testCases { 504 t.Run(testCase.name, func(t *testing.T) { 505 p := Parser{ 506 Accessor: sources, 507 ImportPaths: []string{testCase.cwd, "/foo/bar"}, 508 InferImportPaths: true, 509 } 510 fds, err := p.ParseFiles(testCase.filenames...) 511 testutil.Ok(t, err) 512 testutil.Eq(t, 4, len(fds)) 513 var expectedNames []string 514 if len(testCase.expect) == 0 { 515 expectedNames = []string{"a.proto", "baz/b.proto", "baz/c.proto", "baz/d.proto"} 516 } else { 517 testutil.Eq(t, 4, len(testCase.expect)) 518 expectedNames = testCase.expect 519 } 520 // check that they have the expected name 521 testutil.Eq(t, expectedNames[0], fds[0].GetName()) 522 testutil.Eq(t, expectedNames[1], fds[1].GetName()) 523 testutil.Eq(t, expectedNames[2], fds[2].GetName()) 524 testutil.Eq(t, expectedNames[3], fds[3].GetName()) 525 }) 526 } 527 } 528 529 func TestParseFilesButDoNotLink_DoesNotUseImportPaths(t *testing.T) { 530 tempdir, err := os.MkdirTemp("", "protoparse") 531 testutil.Ok(t, err) 532 defer func() { 533 _ = os.RemoveAll(tempdir) 534 }() 535 err = os.WriteFile(filepath.Join(tempdir, "extra.proto"), []byte("package extra;"), 0644) 536 testutil.Ok(t, err) 537 mainPath := filepath.Join(tempdir, "main.proto") 538 err = os.WriteFile(mainPath, []byte("package main; import \"extra.proto\";"), 0644) 539 testutil.Ok(t, err) 540 p := Parser{ 541 ImportPaths: []string{tempdir}, 542 } 543 fds, err := p.ParseFilesButDoNotLink(mainPath) 544 testutil.Ok(t, err) 545 testutil.Eq(t, 1, len(fds)) 546 }