Skip to content

Commit 6e255d4

Browse files
fix: descriptor extensions handling post-editions (#2075)
1 parent 9467abe commit 6e255d4

File tree

9 files changed

+1998
-228
lines changed

9 files changed

+1998
-228
lines changed

ext/descriptor/index.js

Lines changed: 179 additions & 69 deletions
Large diffs are not rendered by default.

google/protobuf/descriptor.json

Lines changed: 659 additions & 16 deletions
Large diffs are not rendered by default.

google/protobuf/descriptor.proto

Lines changed: 254 additions & 6 deletions
Large diffs are not rendered by default.

src/namespace.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ Namespace.prototype.define = function define(path, json) {
352352
Namespace.prototype.resolveAll = function resolveAll() {
353353
if (!this._needsRecursiveResolve) return this;
354354

355+
this._resolveFeaturesRecursive(this._edition);
356+
355357
var nested = this.nestedArray, i = 0;
356358
this.resolve();
357359
while (i < nested.length)
@@ -408,7 +410,7 @@ Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChe
408410
return this.root.lookup(path.slice(1), filterTypes);
409411

410412
// Early bailout for objects with matching absolute paths
411-
var found = this.root._fullyQualifiedObjects["." + flatPath];
413+
var found = this.root._fullyQualifiedObjects && this.root._fullyQualifiedObjects["." + flatPath];
412414
if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
413415
return found;
414416
}

src/root.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,16 @@ Root.prototype.load = function load(filename, options, callback) {
110110

111111
// Finishes loading by calling the callback (exactly once)
112112
function finish(err, root) {
113-
if (root) {
114-
root.resolveAll();
115-
}
116113
/* istanbul ignore if */
117114
if (!callback) {
118115
return;
119116
}
120117
if (sync) {
121118
throw err;
122119
}
120+
if (root) {
121+
root.resolveAll();
122+
}
123123
var cb = callback;
124124
callback = null;
125125
cb(err, root);
@@ -285,7 +285,6 @@ Root.prototype.resolveAll = function resolveAll() {
285285
throw Error("unresolvable extensions: " + this.deferred.map(function(field) {
286286
return "'extend " + field.extend + "' in " + field.parent.fullName;
287287
}).join(", "));
288-
this._resolveFeaturesRecursive(this._edition);
289288
return Namespace.prototype.resolveAll.call(this);
290289
};
291290

tests/comp_import_extend.js

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,215 @@ Object.defineProperty(exports, "__esModule", { value: true });
33
var path = require("path");
44
var tape = require("tape");
55
var protobuf = require("../index");
6+
67
// to extend Root
7-
require("../ext/descriptor");
8-
tape.test("extensions", function (test) {
8+
var descriptor = require("../ext/descriptor");
9+
10+
tape.test("extensions - proto2 to proto3", function (test) {
911
// load document with extended field imported multiple times
1012
var root = protobuf.loadSync(path.resolve(__dirname, "data/test.proto"));
11-
root.resolveAll();
13+
1214
// convert to Descriptor Set
1315
var decodedDescriptorSet = root.toDescriptor("proto3");
16+
1417
// load back from descriptor set
1518
var root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
19+
20+
var Simple1 = root2.lookup("Simple1");
21+
test.notOk(Simple1.fields.aString.required, "required fields don't exist in proto3");
22+
test.notOk(Simple1.fields.aBoolean.hasPresence, "presence is not preserved");
23+
1624
test.pass("should parse and resolve without errors");
1725
test.end();
1826
});
27+
28+
tape.test("extensions - proto2 roundtrip", function (test) {
29+
// load document with extended field imported multiple times
30+
var root = protobuf.parse(`syntax = "proto2";
31+
32+
message Message {
33+
optional string explicit = 1;
34+
required string required = 2;
35+
repeated int32 packed = 3 [packed = true];
36+
repeated int32 unpacked = 4;
37+
optional int32 repeated_options = 5 [targets = TARGET_TYPE_SERVICE, targets = TARGET_TYPE_FILE];
38+
}
39+
`).root.resolveAll();
40+
41+
// convert to Descriptor Set
42+
var decodedDescriptorSet = root.toDescriptor("proto2");
43+
44+
// load back from descriptor set
45+
var root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
46+
47+
test.same(root.toJSON(), root2.toJSON(), "JSON should roundtrip");
48+
49+
var Message = root2.lookup("Message");
50+
test.ok(Message.fields.required.required, "required field preserved");
51+
test.ok(Message.fields.explicit.hasPresence, "presence is preserved");
52+
test.ok(Message.fields.packed.packed, "packed is preserved");
53+
test.notOk(Message.fields.unpacked.packed, "expanded is preserved");
54+
55+
test.end();
56+
});
57+
58+
tape.test("extensions - proto3 roundtrip", function (test) {
59+
var root = protobuf.parse(`syntax = "proto3";
60+
61+
message Message {
62+
optional string explicit = 1;
63+
string implicit = 2;
64+
repeated int32 packed = 3;
65+
repeated int32 unpacked = 4 [packed = false];
66+
}
67+
`).root.resolveAll();
68+
69+
// convert to Descriptor Set
70+
const decodedDescriptorSet = root.toDescriptor("proto3");
71+
72+
// load back from descriptor set
73+
const root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
74+
75+
var Message = root2.lookup("Message");
76+
77+
test.same(root2.toJSON(), root.toJSON(), "JSON should roundtrip");
78+
79+
test.ok(Message.fields.explicit.hasPresence, "should have explicit presence");
80+
test.notOk(Message.fields.implicit.hasPresence, "should have implicit presence");
81+
test.ok(Message.fields.packed.packed, "packed is preserved");
82+
test.notOk(Message.fields.unpacked.packed, "expanded is preserved");
83+
84+
test.end();
85+
});
86+
87+
tape.test("extensions - edition 2023 file roundtrip", function (test) {
88+
var json = {
89+
nested: { Message: {
90+
edition: "2023",
91+
options: { "features": { "field_presence": "IMPLICIT" } },
92+
fields: {
93+
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
94+
implicit: { type: "string", id: 2 },
95+
required: { type: "string", id: 3, options: { "features": { "field_presence": "LEGACY_REQUIRED" }} },
96+
},
97+
nested: { Nested: { fields: {
98+
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
99+
implicit: { type: "string", id: 2 },
100+
} } }
101+
} }
102+
};
103+
var root = protobuf.Root.fromJSON(json);
104+
105+
// convert to Descriptor Set
106+
const decodedDescriptorSet = root.toDescriptor("2023");
107+
108+
// load back from descriptor set
109+
const root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
110+
111+
var Type = root2.lookup("Message");
112+
var Nested = Type.nested.Nested;
113+
114+
test.same(root2.toJSON(), json, "JSON should roundtrip");
115+
116+
test.ok(Type.fields.explicit.hasPresence, "should have explicit presence");
117+
test.notOk(Type.fields.implicit.hasPresence, "should have implicit presence");
118+
119+
test.ok(Nested.fields.explicit.hasPresence, "nested should have explicit presence");
120+
test.notOk(Nested.fields.implicit.hasPresence, "nested should have implicit presence");
121+
122+
test.end();
123+
});
124+
125+
126+
tape.test("extensions - proto2 root-less type", function (test) {
127+
var Message = protobuf.Type.fromJSON("Message", {
128+
"edition": "proto2",
129+
"fields": {
130+
"explicit": {
131+
"type": "string",
132+
"id": 1
133+
},
134+
"required": {
135+
"rule": "required",
136+
"type": "string",
137+
"id": 2
138+
},
139+
"packed": {
140+
"rule": "repeated",
141+
"type": "int32",
142+
"id": 3,
143+
"options": {
144+
"packed": true
145+
}
146+
},
147+
"unpacked": {
148+
"rule": "repeated",
149+
"type": "int32",
150+
"id": 4
151+
},
152+
"nested": {
153+
"type": "Nested",
154+
"id": 5
155+
}
156+
},
157+
"nested": {
158+
"Nested": {
159+
"fields": {
160+
"a": {
161+
"type": "int32",
162+
"id": 1
163+
}
164+
}
165+
}
166+
}
167+
}).resolveAll();
168+
169+
// convert to Descriptor Set
170+
const decodedDescriptorSet = Message.toDescriptor("proto2");
171+
172+
// load back from descriptor set
173+
const Message2 = protobuf.Type.fromDescriptor(decodedDescriptorSet, "proto2").resolveAll();
174+
175+
test.same(Message2.toJSON(), Message.toJSON(), "JSON should roundtrip");
176+
177+
test.ok(Message2.fields.explicit.hasPresence, "should have explicit presence");
178+
test.ok(Message2.fields.required.required, "should have required presence");
179+
test.ok(Message2.fields.packed.packed, "should have packed encoding");
180+
test.notOk(Message2.fields.unpacked.packed, "should have expanded encoding");
181+
test.same(Message2.fields.nested.resolvedType, Message2.nested.Nested, "should have cross linkage");
182+
183+
test.end();
184+
});
185+
186+
187+
tape.test("extensions - unsupported edition", function (test) {
188+
var json = {
189+
nested: { Message: {
190+
edition: "2023",
191+
options: { "features": { "field_presence": "IMPLICIT" } },
192+
fields: {
193+
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
194+
implicit: { type: "string", id: 2 },
195+
},
196+
nested: { Nested: { fields: {
197+
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
198+
implicit: { type: "string", id: 2 },
199+
} } }
200+
} }
201+
};
202+
var root = protobuf.Root.fromJSON(json);
203+
204+
// convert to Descriptor Set
205+
test.throws(function() {
206+
root.toDescriptor("2030")
207+
}, /Unsupported edition 2030/, "unsupported edition output throws");
208+
209+
const decodedDescriptorSet = root.toDescriptor("2023");
210+
decodedDescriptorSet.file[0].edition = descriptor.Edition.EDITION_99997_TEST_ONLY
211+
212+
test.throws(function() {
213+
protobuf.Root.fromDescriptor(decodedDescriptorSet)
214+
}, /Unsupported edition 99997/, "unsupported edition input throws");
215+
216+
test.end();
217+
});

tests/comp_parse-uncommon.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ function traverseTypes(current, fn) {
3434
traverseTypes(nested, fn);
3535
});
3636
}
37+
38+
tape.test("invalid lookup", async function(test) {
39+
try {
40+
await protobuf.load("tests/data/invalid-lookup.proto");
41+
test.fail("should have thrown");
42+
} catch(err) {
43+
test.match(err.message, /illegal token 'required'/, "failed to parse");
44+
}
45+
});
46+

0 commit comments

Comments
 (0)