1package protoparse
2
3import (
4	"strings"
5	"testing"
6
7	"github.com/jhump/protoreflect/internal/testutil"
8)
9
10func TestBasicValidation(t *testing.T) {
11	testCases := []struct {
12		contents string
13		succeeds bool
14		errMsg   string
15	}{
16		{
17			contents: `message Foo { optional double bar = 1 [default = -18446744073709551615]; }`,
18			succeeds: true,
19		},
20		{
21			// with byte order marker
22			contents: string([]byte{0xEF, 0xBB, 0xBF}) + `message Foo { optional double bar = 1 [default = -18446744073709551615]; }`,
23			succeeds: true,
24		},
25		{
26			contents: `message Foo { optional double bar = 1 [default = 18446744073709551616]; }`,
27			succeeds: true,
28		},
29		{
30			contents: `message Foo { oneof bar { group Baz = 1 [deprecated=true] { optional int abc = 1; } } }`,
31			succeeds: true,
32		},
33		{
34			contents: `message Foo { option message_set_wire_format = true; extensions 1 to 100; }`,
35			succeeds: true,
36		},
37		{
38			contents: `message Foo { optional double bar = 536870912; option message_set_wire_format = true; }`,
39			errMsg:   "test.proto:1:15: messages with message-set wire format cannot contain non-extension fields",
40		},
41		{
42			contents: `message Foo { option message_set_wire_format = true; }`,
43			errMsg:   "test.proto:1:15: messages with message-set wire format must contain at least one extension range",
44		},
45		{
46			contents: `syntax = "proto1";`,
47			errMsg:   `test.proto:1:10: syntax value must be "proto2" or "proto3"`,
48		},
49		{
50			contents: `message Foo { optional string s = 5000000000; }`,
51			errMsg:   `test.proto:1:35: tag number 5000000000 is higher than max allowed tag number (536870911)`,
52		},
53		{
54			contents: `message Foo { optional string s = 19500; }`,
55			errMsg:   `test.proto:1:35: tag number 19500 is in disallowed reserved range 19000-19999`,
56		},
57		{
58			contents: `enum Foo { V = 5000000000; }`,
59			errMsg:   `test.proto:1:16: value 5000000000 is out of range: should be between -2147483648 and 2147483647`,
60		},
61		{
62			contents: `enum Foo { V = -5000000000; }`,
63			errMsg:   `test.proto:1:16: value -5000000000 is out of range: should be between -2147483648 and 2147483647`,
64		},
65		{
66			contents: `enum Foo { V = 0; reserved 5000000000; }`,
67			errMsg:   `test.proto:1:28: range start 5000000000 is out of range: should be between -2147483648 and 2147483647`,
68		},
69		{
70			contents: `enum Foo { V = 0; reserved -5000000000; }`,
71			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
72		},
73		{
74			contents: `enum Foo { V = 0; reserved 5000000000 to 5000000001; }`,
75			errMsg:   `test.proto:1:28: range start 5000000000 is out of range: should be between -2147483648 and 2147483647`,
76		},
77		{
78			contents: `enum Foo { V = 0; reserved 5 to 5000000000; }`,
79			errMsg:   `test.proto:1:33: range end 5000000000 is out of range: should be between -2147483648 and 2147483647`,
80		},
81		{
82			contents: `enum Foo { V = 0; reserved -5000000000 to -5; }`,
83			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
84		},
85		{
86			contents: `enum Foo { V = 0; reserved -5000000001 to -5000000000; }`,
87			errMsg:   `test.proto:1:28: range start -5000000001 is out of range: should be between -2147483648 and 2147483647`,
88		},
89		{
90			contents: `enum Foo { V = 0; reserved -5000000000 to 5; }`,
91			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
92		},
93		{
94			contents: `enum Foo { V = 0; reserved -5 to 5000000000; }`,
95			errMsg:   `test.proto:1:34: range end 5000000000 is out of range: should be between -2147483648 and 2147483647`,
96		},
97		{
98			contents: `enum Foo { }`,
99			errMsg:   `test.proto:1:1: enum Foo: enums must define at least one value`,
100		},
101		{
102			contents: `message Foo { oneof Bar { } }`,
103			errMsg:   `test.proto:1:15: oneof must contain at least one field`,
104		},
105		{
106			contents: `message Foo { extensions 1 to max; } extend Foo { }`,
107			errMsg:   `test.proto:1:38: extend sections must define at least one extension`,
108		},
109		{
110			contents: `message Foo { option map_entry = true; }`,
111			errMsg:   `test.proto:1:34: message Foo: map_entry option should not be set explicitly; use map type instead`,
112		},
113		{
114			contents: `message Foo { option map_entry = false; }`,
115			succeeds: true, // okay if explicit setting is false
116		},
117		{
118			contents: `syntax = "proto2"; message Foo { string s = 1; }`,
119			errMsg:   `test.proto:1:41: field Foo.s: field has no label; proto2 requires explicit 'optional' label`,
120		},
121		{
122			contents: `message Foo { string s = 1; }`, // syntax defaults to proto2
123			errMsg:   `test.proto:1:22: field Foo.s: field has no label; proto2 requires explicit 'optional' label`,
124		},
125		{
126			contents: `syntax = "proto3"; message Foo { optional string s = 1; }`,
127			succeeds: true, // proto3_optional
128		},
129		{
130			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; extend google.protobuf.MessageOptions { optional string s = 50000; }`,
131			succeeds: true, // proto3_optional for extensions
132		},
133		{
134			contents: `syntax = "proto3"; message Foo { required string s = 1; }`,
135			errMsg:   `test.proto:1:34: field Foo.s: label 'required' is not allowed in proto3`,
136		},
137		{
138			contents: `message Foo { extensions 1 to max; } extend Foo { required string sss = 100; }`,
139			errMsg:   `test.proto:1:51: field sss: extension fields cannot be 'required'`,
140		},
141		{
142			contents: `syntax = "proto3"; message Foo { optional group Grp = 1 { } }`,
143			errMsg:   `test.proto:1:43: field Foo.grp: groups are not allowed in proto3`,
144		},
145		{
146			contents: `syntax = "proto3"; message Foo { extensions 1 to max; }`,
147			errMsg:   `test.proto:1:45: message Foo: extension ranges are not allowed in proto3`,
148		},
149		{
150			contents: `syntax = "proto3"; message Foo { string s = 1 [default = "abcdef"]; }`,
151			errMsg:   `test.proto:1:48: field Foo.s: default values are not allowed in proto3`,
152		},
153		{
154			contents: `enum Foo { V1 = 1; V2 = 1; }`,
155			errMsg:   `test.proto:1:25: enum Foo: values V1 and V2 both have the same numeric value 1; use allow_alias option if intentional`,
156		},
157		{
158			contents: `enum Foo { option allow_alias = true; V1 = 1; V2 = 1; }`,
159			succeeds: true,
160		},
161		{
162			contents: `enum Foo { option allow_alias = false; V1 = 1; V2 = 2; }`,
163			succeeds: true,
164		},
165		{
166			contents: `enum Foo { option allow_alias = true; V1 = 1; V2 = 2; }`,
167			errMsg:   `test.proto:1:33: enum Foo: allow_alias is true but no values are aliases`,
168		},
169		{
170			contents: `syntax = "proto3"; enum Foo { V1 = 0; reserved 1 to 20; reserved "V2"; }`,
171			succeeds: true,
172		},
173		{
174			contents: `enum Foo { V1 = 1; reserved 1 to 20; reserved "V2"; }`,
175			errMsg:   `test.proto:1:17: enum Foo: value V1 is using number 1 which is in reserved range 1 to 20`,
176		},
177		{
178			contents: `enum Foo { V1 = 20; reserved 1 to 20; reserved "V2"; }`,
179			errMsg:   `test.proto:1:17: enum Foo: value V1 is using number 20 which is in reserved range 1 to 20`,
180		},
181		{
182			contents: `enum Foo { V2 = 0; reserved 1 to 20; reserved "V2"; }`,
183			errMsg:   `test.proto:1:12: enum Foo: value V2 is using a reserved name`,
184		},
185		{
186			contents: `enum Foo { V0 = 0; reserved 1 to 20; reserved 21 to 40; reserved "V2"; }`,
187			succeeds: true,
188		},
189		{
190			contents: `enum Foo { V0 = 0; reserved 1 to 20; reserved 20 to 40; reserved "V2"; }`,
191			errMsg:   `test.proto:1:47: enum Foo: reserved ranges overlap: 1 to 20 and 20 to 40`,
192		},
193		{
194			contents: `syntax = "proto3"; enum Foo { FIRST = 1; }`,
195			errMsg:   `test.proto:1:39: enum Foo: proto3 requires that first value in enum have numeric value of 0`,
196		},
197		{
198			contents: `syntax = "proto3"; message Foo { string s = 1; int32 i = 1; }`,
199			errMsg:   `test.proto:1:58: message Foo: fields s and i both have the same tag 1`,
200		},
201		{
202			contents: `message Foo { reserved 1 to 10, 10 to 12; }`,
203			errMsg:   `test.proto:1:33: message Foo: reserved ranges overlap: 1 to 10 and 10 to 12`,
204		},
205		{
206			contents: `message Foo { extensions 1 to 10, 10 to 12; }`,
207			errMsg:   `test.proto:1:35: message Foo: extension ranges overlap: 1 to 10 and 10 to 12`,
208		},
209		{
210			contents: `message Foo { reserved 1 to 10; extensions 10 to 12; }`,
211			errMsg:   `test.proto:1:44: message Foo: extension range 10 to 12 overlaps reserved range 1 to 10`,
212		},
213		{
214			contents: `message Foo { reserved 1, 2 to 10, 11 to 20; extensions 21 to 22; }`,
215			succeeds: true,
216		},
217		{
218			contents: `message Foo { reserved 10 to 1; }`,
219			errMsg:   `test.proto:1:24: range, 10 to 1, is invalid: start must be <= end`,
220		},
221		{
222			contents: `message Foo { extensions 10 to 1; }`,
223			errMsg:   `test.proto:1:26: range, 10 to 1, is invalid: start must be <= end`,
224		},
225		{
226			contents: `message Foo { reserved 1 to 5000000000; }`,
227			errMsg:   `test.proto:1:29: range end 5000000000 is out of range: should be between 0 and 536870911`,
228		},
229		{
230			contents: `message Foo { extensions 3000000000; }`,
231			errMsg:   `test.proto:1:26: range start 3000000000 is out of range: should be between 0 and 536870911`,
232		},
233		{
234			contents: `message Foo { extensions 3000000000 to 3000000001; }`,
235			errMsg:   `test.proto:1:26: range start 3000000000 is out of range: should be between 0 and 536870911`,
236		},
237		{
238			contents: `message Foo { extensions 100 to 3000000000; }`,
239			errMsg:   `test.proto:1:33: range end 3000000000 is out of range: should be between 0 and 536870911`,
240		},
241		{
242			contents: `message Foo { reserved "foo", "foo"; }`,
243			errMsg:   `test.proto:1:31: name "foo" is reserved multiple times`,
244		},
245		{
246			contents: `message Foo { reserved "foo"; reserved "foo"; }`,
247			errMsg:   `test.proto:1:40: name "foo" is reserved multiple times`,
248		},
249		{
250			contents: `message Foo { reserved "foo"; optional string foo = 1; }`,
251			errMsg:   `test.proto:1:47: message Foo: field foo is using a reserved name`,
252		},
253		{
254			contents: `message Foo { reserved 1 to 10; optional string foo = 1; }`,
255			errMsg:   `test.proto:1:55: message Foo: field foo is using tag 1 which is in reserved range 1 to 10`,
256		},
257		{
258			contents: `message Foo { extensions 1 to 10; optional string foo = 1; }`,
259			errMsg:   `test.proto:1:57: message Foo: field foo is using tag 1 which is in extension range 1 to 10`,
260		},
261		{
262			contents: `message Foo { optional group foo = 1 { } }`,
263			errMsg:   `test.proto:1:30: group foo should have a name that starts with a capital letter`,
264		},
265		{
266			contents: `message Foo { oneof foo { group bar = 1 { } } }`,
267			errMsg:   `test.proto:1:33: group bar should have a name that starts with a capital letter`,
268		},
269		{
270			contents: `enum Foo { option = 1; }`,
271			errMsg:   `test.proto:1:19: syntax error: unexpected '='`,
272		},
273		{
274			contents: `enum Foo { reserved = 1; }`,
275			errMsg:   `test.proto:1:21: syntax error: unexpected '=', expecting string literal or int literal or '-'`,
276		},
277		{
278			contents: `syntax = "proto3"; enum message { unset = 0; } message Foo { message bar = 1; }`,
279			errMsg:   `test.proto:1:74: syntax error: unexpected '=', expecting '{'`,
280		},
281		{
282			contents: `syntax = "proto3"; enum enum { unset = 0; } message Foo { enum bar = 1; }`,
283			errMsg:   `test.proto:1:68: syntax error: unexpected '=', expecting '{'`,
284		},
285		{
286			contents: `syntax = "proto3"; enum reserved { unset = 0; } message Foo { reserved bar = 1; }`,
287			errMsg:   `test.proto:1:72: syntax error: unexpected identifier, expecting string literal or int literal`,
288		},
289		{
290			contents: `syntax = "proto3"; enum extend { unset = 0; } message Foo { extend bar = 1; }`,
291			errMsg:   `test.proto:1:72: syntax error: unexpected '=', expecting '{'`,
292		},
293		{
294			contents: `syntax = "proto3"; enum oneof { unset = 0; } message Foo { oneof bar = 1; }`,
295			errMsg:   `test.proto:1:70: syntax error: unexpected '=', expecting '{'`,
296		},
297		{
298			contents: `syntax = "proto3"; enum optional { unset = 0; } message Foo { optional bar = 1; }`,
299			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
300		},
301		{
302			contents: `syntax = "proto3"; enum repeated { unset = 0; } message Foo { repeated bar = 1; }`,
303			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
304		},
305		{
306			contents: `syntax = "proto3"; enum required { unset = 0; } message Foo { required bar = 1; }`,
307			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
308		},
309		{
310			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum optional { unset = 0; } extend google.protobuf.MethodOptions { optional bar = 22222; }`,
311			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
312		},
313		{
314			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum repeated { unset = 0; } extend google.protobuf.MethodOptions { repeated bar = 22222; }`,
315			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
316		},
317		{
318			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum required { unset = 0; } extend google.protobuf.MethodOptions { required bar = 22222; }`,
319			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
320		},
321		{
322			contents: `syntax = "proto3"; enum optional { unset = 0; } message Foo { oneof bar { optional bar = 1; } }`,
323			errMsg:   `test.proto:1:75: syntax error: unexpected "optional"`,
324		},
325		{
326			contents: `syntax = "proto3"; enum repeated { unset = 0; } message Foo { oneof bar { repeated bar = 1; } }`,
327			errMsg:   `test.proto:1:75: syntax error: unexpected "repeated"`,
328		},
329		{
330			contents: `syntax = "proto3"; enum required { unset = 0; } message Foo { oneof bar { required bar = 1; } }`,
331			errMsg:   `test.proto:1:75: syntax error: unexpected "required"`,
332		},
333		{
334			contents: ``,
335			succeeds: true,
336		},
337		{
338			contents: `0`,
339			errMsg:   `test.proto:1:1: syntax error: unexpected int literal`,
340		},
341		{
342			contents: `foobar`,
343			errMsg:   `test.proto:1:1: syntax error: unexpected identifier`,
344		},
345		{
346			contents: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`,
347			errMsg:   `test.proto:1:1: syntax error: unexpected identifier`,
348		},
349		{
350			contents: `"abc"`,
351			errMsg:   `test.proto:1:1: syntax error: unexpected string literal`,
352		},
353		{
354			contents: `0.0.0.0.0`,
355			errMsg:   `test.proto:1:1: invalid syntax in float value: 0.0.0.0.0`,
356		},
357		{
358			contents: `0.0`,
359			errMsg:   `test.proto:1:1: syntax error: unexpected float literal`,
360		},
361	}
362
363	for i, tc := range testCases {
364		errs := newErrorHandler(nil, nil)
365		_ = parseProto("test.proto", strings.NewReader(tc.contents), errs, true, true)
366		err := errs.getError()
367		if tc.succeeds {
368			testutil.Ok(t, err, "case #%d should succeed", i)
369		} else {
370			testutil.Nok(t, err, "case #%d should fail", i)
371			testutil.Eq(t, tc.errMsg, err.Error(), "case #%d bad error message", i)
372		}
373	}
374}
375