Files
protobuf.js/tests/api_descriptor.js

673 lines
26 KiB
JavaScript

var path = require("path");
var tape = require("tape");
var protobuf = require("../index");
var descriptor = require("../ext/descriptor");
tape.test("descriptor - bundled common field names", function(test) {
var root = new protobuf.Root().loadSync("google/protobuf/struct.proto", { keepCase: true });
var descriptorSet = root.toDescriptor("proto3");
var value = descriptorSet.file[0].messageType.filter(function(type) {
return type.name === "Value";
})[0];
test.same(value.field.map(function(field) {
return field.name;
}), [ "null_value", "number_value", "string_value", "bool_value", "struct_value", "list_value" ], "uses original proto field names");
test.end();
});
tape.test("descriptor - proto2 to proto3", function (test) {
// load document with extended field imported multiple times
var root = protobuf.loadSync(path.resolve(__dirname, "data/test.proto"));
// convert to Descriptor Set
var decodedDescriptorSet = root.toDescriptor("proto3");
// load back from descriptor set
var root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
var Simple1 = root2.lookup("Simple1");
test.notOk(Simple1.fields.aString.required, "required fields don't exist in proto3");
test.notOk(Simple1.fields.aBoolean.hasPresence, "presence is not preserved");
test.pass("should parse and resolve without errors");
test.end();
});
tape.test("descriptor - proto2 roundtrip", function (test) {
// load document with extended field imported multiple times
var root = protobuf.parse(`syntax = "proto2";
message Message {
optional string explicit = 1;
required string required = 2;
repeated int32 packed = 3 [packed = true];
repeated int32 unpacked = 4;
optional int32 repeated_options = 5 [targets = TARGET_TYPE_SERVICE, targets = TARGET_TYPE_FILE];
}
`).root.resolveAll();
// convert to Descriptor Set
var decodedDescriptorSet = root.toDescriptor("proto2");
// load back from descriptor set
var root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
test.same(root.toJSON(), root2.toJSON(), "JSON should roundtrip");
var Message = root2.lookup("Message");
test.ok(Message.fields.required.required, "required field preserved");
test.ok(Message.fields.explicit.hasPresence, "presence is preserved");
test.ok(Message.fields.packed.packed, "packed is preserved");
test.notOk(Message.fields.unpacked.packed, "expanded is preserved");
test.end();
});
tape.test("descriptor - numeric defaults", function(test) {
var root = protobuf.Root.fromDescriptor(descriptor.FileDescriptorSet.create({
file: [{
name: "defaults.proto",
syntax: "proto2",
messageType: [{
name: "Message",
field: [
{ name: "floatValue", number: 1, label: 1, type: 2, defaultValue: "8.999999488e+09" },
{ name: "doubleValue", number: 2, label: 1, type: 1, defaultValue: "7e+22" },
{ name: "negativeZero", number: 3, label: 1, type: 2, defaultValue: "-0" },
{ name: "positiveInfinity", number: 4, label: 1, type: 1, defaultValue: "inf" },
{ name: "negativeInfinity", number: 5, label: 1, type: 1, defaultValue: "-inf" },
{ name: "notANumber", number: 6, label: 1, type: 1, defaultValue: "nan" },
{ name: "int32Value", number: 7, label: 1, type: 5, defaultValue: "-123456789" },
{ name: "int64Value", number: 8, label: 1, type: 3, defaultValue: "-9123456789123456789" },
{ name: "uint64Value", number: 9, label: 1, type: 4, defaultValue: "10123456789123456789" },
{ name: "stringValue", number: 10, label: 1, type: 9, defaultValue: "123" }
]
}]
}]
})).resolveAll();
var Message = root.lookupType("Message"),
message = Message.create();
test.equal(message.floatValue, 8999999488, "parses an exponent float default");
test.equal(message.doubleValue, 7e22, "parses an exponent double default");
test.ok(Object.is(message.negativeZero, -0), "preserves a negative zero float default");
test.equal(message.positiveInfinity, Infinity, "parses a positive infinity default");
test.equal(message.negativeInfinity, -Infinity, "parses a negative infinity default");
test.ok(Number.isNaN(message.notANumber), "parses a nan default");
test.equal(message.int32Value, -123456789, "parses a negative int32 default");
test.equal(message.int64Value.toString(), "-9123456789123456789", "parses an exact int64 default");
test.equal(message.uint64Value.toString(), "10123456789123456789", "parses an exact uint64 default");
test.equal(message.stringValue, "123", "keeps a numeric-looking string default");
test.throws(function() {
protobuf.Field.fromDescriptor(descriptor.FieldDescriptorProto.create({
name: "value", number: 1, label: 1, type: 1, defaultValue: "7junk"
}));
}, /illegal default value for double/, "rejects an invalid floating-point default");
test.throws(function() {
protobuf.Field.fromDescriptor(descriptor.FieldDescriptorProto.create({
name: "value", number: 1, label: 1, type: 3, defaultValue: "7junk"
}));
}, /illegal default value for int64/, "rejects an invalid integer default");
test.end();
});
tape.test("descriptor - ranges use descriptor end semantics at boundary", function (test) {
var root = protobuf.parse(`syntax = "proto2";
message Message {
extensions 100 to 199;
reserved 2, 9 to 11;
reserved "old";
optional string value = 1;
enum Values {
reserved -2 to -1, 2, 40 to max;
reserved "OLD";
ZERO = 0;
OK = 1;
}
}
`).root.resolveAll();
var descriptorSet = root.toDescriptor("proto2"),
descriptorMessage = descriptorSet.file[0].messageType[0],
descriptorEnum = descriptorMessage.enumType[0];
test.same(root.lookupType("Message").extensions, [[100, 199]], "extensions are inclusive internally");
test.same(root.lookupType("Message").reserved, [[2, 2], [9, 11], "old"], "message reserved ranges are inclusive internally");
test.same(root.lookupEnum("Message.Values").reserved, [[-2, -1], [2, 2], [40, 2147483647], "OLD"], "enum reserved ranges are inclusive internally");
test.same(descriptorMessage.extensionRange.map(function(range) {
return [range.start, range.end];
}), [[100, 200]], "message extension descriptors use exclusive end");
test.same(descriptorMessage.reservedRange.map(function(range) {
return [range.start, range.end];
}), [[2, 3], [9, 12]], "message reserved descriptors use exclusive end");
test.same(descriptorEnum.reservedRange.map(function(range) {
return [range.start, range.end];
}), [[-2, -1], [2, 2], [40, 2147483647]], "enum reserved descriptors keep inclusive end");
test.same(descriptorEnum.reservedName, ["OLD"], "enum reserved names are emitted");
test.same(JSON.parse(JSON.stringify(protobuf.Root.fromDescriptor(descriptorSet).toJSON())), JSON.parse(JSON.stringify(root.toJSON())), "JSON should roundtrip");
test.end();
});
tape.test("descriptor - proto3 roundtrip", function (test) {
var root = protobuf.parse(`syntax = "proto3";
message Message {
optional string explicit = 1;
string implicit = 2;
repeated int32 packed = 3;
repeated int32 unpacked = 4 [packed = false];
}
`).root.resolveAll();
// convert to Descriptor Set
const decodedDescriptorSet = root.toDescriptor("proto3");
// load back from descriptor set
const root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
var Message = root2.lookup("Message");
test.same(root2.toJSON(), root.toJSON(), "JSON should roundtrip");
test.ok(Message.fields.explicit.hasPresence, "should have explicit presence");
test.notOk(Message.fields.implicit.hasPresence, "should have implicit presence");
test.ok(Message.fields.packed.packed, "packed is preserved");
test.notOk(Message.fields.unpacked.packed, "expanded is preserved");
test.end();
});
tape.test("descriptor - proto3 optional in extend toDescriptor", function (test) {
var root = protobuf.parse(`syntax = "proto3";
message SomeMessage {
string foo = 1;
}
extend SomeMessage {
optional string bar = 2;
}
`).root.resolveAll();
var decodedDescriptorSet;
try {
decodedDescriptorSet = root.toDescriptor("proto3");
test.pass("toDescriptor should not throw");
} catch (err) {
test.fail(err);
test.end();
return;
}
test.ok(decodedDescriptorSet.file[0].extension && decodedDescriptorSet.file[0].extension.length === 1,
"should include extension field");
var encodedDescriptorSet = descriptor.FileDescriptorSet.decode(
descriptor.FileDescriptorSet.encode(decodedDescriptorSet).finish()
);
var ext = encodedDescriptorSet.file[0].extension[0];
test.equal(ext.name, "bar", "extension field name is preserved");
test.equal(ext.proto3Optional, true, "proto3 optional flag is preserved");
test.notOk(Object.prototype.hasOwnProperty.call(ext, "proto3_optional"), "does not emit legacy snake_case descriptor property");
var root2 = protobuf.Root.fromDescriptor(encodedDescriptorSet),
ext2 = root2.lookup("bar");
test.equal(ext2.options.proto3_optional, true, "proto3 optional flag is restored");
test.end();
});
tape.test("descriptor - edition 2023 file roundtrip", function (test) {
var json = {
nested: { Message: {
edition: "2023",
options: { "features": { "field_presence": "IMPLICIT" } },
fields: {
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
implicit: { type: "string", id: 2 },
required: { type: "string", id: 3, options: { "features": { "field_presence": "LEGACY_REQUIRED" }} },
},
nested: { Nested: { fields: {
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
implicit: { type: "string", id: 2 },
} } }
} }
};
var root = protobuf.Root.fromJSON(json);
// convert to Descriptor Set
const decodedDescriptorSet = root.toDescriptor("2023");
// load back from descriptor set
const root2 = protobuf.Root.fromDescriptor(decodedDescriptorSet);
var Type = root2.lookup("Message");
var Nested = Type.nested.Nested;
test.same(root2.toJSON(), json, "JSON should roundtrip");
test.ok(Type.fields.explicit.hasPresence, "should have explicit presence");
test.notOk(Type.fields.implicit.hasPresence, "should have implicit presence");
test.ok(Nested.fields.explicit.hasPresence, "nested should have explicit presence");
test.notOk(Nested.fields.implicit.hasPresence, "nested should have implicit presence");
test.end();
});
tape.test("descriptor - imports file-level edition features", function(test) {
var root = protobuf.parse(`edition = "2023";
option features.repeated_field_encoding = EXPANDED;
message Message {
repeated int32 expanded = 1;
repeated int32 packed = 2 [features.repeated_field_encoding = PACKED];
message Nested {
repeated int32 expanded = 1;
}
}
`).root.resolveAll();
var root2 = protobuf.Root.fromDescriptor(root.toDescriptor("2023")),
Message = root2.lookupType("Message"),
Nested = root2.lookupType("Message.Nested");
test.notOk(Message.fields.expanded.packed, "uses file-level expanded default");
test.ok(Message.fields.packed.packed, "keeps field-level packed override");
test.notOk(Nested.fields.expanded.packed, "nested field inherits file-level default");
test.end();
});
tape.test("descriptor - imports legacy group metadata", function(test) {
var root = protobuf.parse(`syntax = "proto2";
message WithGroup {
optional group MyGroup = 1 {
optional int32 value = 2;
}
}
`).root.resolveAll();
var root2 = protobuf.Root.fromDescriptor(root.toDescriptor("proto2")),
Group = root2.lookupType("WithGroup.MyGroup"),
field = root2.lookupType("WithGroup").fields.myGroup;
test.ok(Group.group, "marks group type");
test.ok(field.delimited, "infers delimited field encoding");
test.end();
});
tape.test("descriptor - proto2 root-less type", function (test) {
var Message = protobuf.Type.fromJSON("Message", {
"edition": "proto2",
"fields": {
"explicit": {
"type": "string",
"id": 1
},
"required": {
"rule": "required",
"type": "string",
"id": 2
},
"packed": {
"rule": "repeated",
"type": "int32",
"id": 3,
"options": {
"packed": true
}
},
"unpacked": {
"rule": "repeated",
"type": "int32",
"id": 4
},
"nested": {
"type": "Nested",
"id": 5
}
},
"nested": {
"Nested": {
"fields": {
"a": {
"type": "int32",
"id": 1
}
}
}
}
}).resolveAll();
// convert to Descriptor Set
const decodedDescriptorSet = Message.toDescriptor("proto2");
// load back from descriptor set
const Message2 = protobuf.Type.fromDescriptor(decodedDescriptorSet, "proto2").resolveAll();
test.same(Message2.toJSON(), Message.toJSON(), "JSON should roundtrip");
test.ok(Message2.fields.explicit.hasPresence, "should have explicit presence");
test.ok(Message2.fields.required.required, "should have required presence");
test.ok(Message2.fields.packed.packed, "should have packed encoding");
test.notOk(Message2.fields.unpacked.packed, "should have expanded encoding");
test.same(Message2.fields.nested.resolvedType, Message2.nested.Nested, "should have cross linkage");
test.end();
});
tape.test("descriptor - unsupported edition", function (test) {
var json = {
nested: { Message: {
edition: "2023",
options: { "features": { "field_presence": "IMPLICIT" } },
fields: {
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
implicit: { type: "string", id: 2 },
},
nested: { Nested: { fields: {
explicit: { type: "string", id: 1, options: { "features": { "field_presence": "EXPLICIT" } } },
implicit: { type: "string", id: 2 },
} } }
} }
};
var root = protobuf.Root.fromJSON(json);
// convert to Descriptor Set
test.throws(function() {
root.toDescriptor("2030")
}, /Unsupported edition 2030/, "unsupported edition output throws");
const decodedDescriptorSet = root.toDescriptor("2023");
decodedDescriptorSet.file[0].edition = descriptor.Edition.EDITION_99997_TEST_ONLY
test.throws(function() {
protobuf.Root.fromDescriptor(decodedDescriptorSet)
}, /Unsupported edition 99997/, "unsupported edition input throws");
test.end();
});
tape.test("descriptor - descriptor type names", function(test) {
var field = descriptor.FieldDescriptorProto.create({
name: "field",
number: 1,
label: 1,
type: 11,
typeName: ".pkg.Message"
});
test.equal(protobuf.Field.fromDescriptor(field).type, ".pkg.Message", "should accept qualified type names");
field.typeName = ".pkg.Message;bad";
test.throws(function() {
protobuf.Field.fromDescriptor(field);
}, /illegal type name/, "should reject invalid type names");
field.typeName = ".pkg.Message";
field.extendee = ".pkg.Message;bad";
test.throws(function() {
protobuf.Field.fromDescriptor(field);
}, /illegal type name/, "should reject invalid extendee names");
var method = descriptor.MethodDescriptorProto.create({
name: "Call",
inputType: ".pkg.Request",
outputType: ".pkg.Response"
});
test.equal(protobuf.Method.fromDescriptor(method).requestType, ".pkg.Request", "should accept method type names");
method.inputType = ".pkg.Request;bad";
test.throws(function() {
protobuf.Method.fromDescriptor(method);
}, /illegal type name/, "should reject invalid method type names");
test.end();
});
tape.test("descriptor - encoded descriptor inputs", function(test) {
var root = protobuf.parse("syntax = \"proto3\"; message Message { string value = 1; }").root.resolveAll(),
descriptorSet = root.toDescriptor("proto3"),
descriptorBytes = descriptor.FileDescriptorSet.encode(descriptorSet).finish(),
fieldDescriptor = descriptor.FieldDescriptorProto.create({
name: "value",
number: 1,
label: 1,
type: 9
}),
fieldBytes = descriptor.FieldDescriptorProto.encode(fieldDescriptor).finish();
test.equal(protobuf.Root.fromDescriptor(descriptorBytes).lookupType("Message").fields.value.type, "string", "decodes root descriptor buffers");
test.equal(protobuf.Root.fromDescriptor(protobuf.Reader.create(descriptorBytes)).lookupType("Message").fields.value.type, "string", "decodes root descriptor readers");
test.equal(protobuf.Field.fromDescriptor(fieldBytes).type, "string", "decodes field descriptor buffers");
test.equal(protobuf.Field.fromDescriptor(protobuf.Reader.create(fieldBytes)).type, "string", "decodes field descriptor readers");
test.end();
});
tape.test("descriptor - nesting", function(test) {
function nestedMessageDescriptor(depth) {
var root = descriptor.DescriptorProto.create({ name: "Message0" }),
nested = root;
for (var i = 1; i <= depth; ++i) {
var child = descriptor.DescriptorProto.create({ name: "Message" + i });
nested.nestedType.push(child);
nested = child;
}
return root;
}
function packageDescriptor(depth) {
var packageName = "a";
for (var i = 1; i < depth; ++i)
packageName += ".a";
return descriptor.FileDescriptorSet.create({
file: [ descriptor.FileDescriptorProto.create({
name: "test.proto",
"package": packageName
}) ]
});
}
var recursionLimit = protobuf.util.recursionLimit,
nestingLimit = protobuf.util.nestingLimit;
try {
protobuf.util.recursionLimit = 100;
protobuf.util.nestingLimit = 3;
test.doesNotThrow(function() {
protobuf.Type.fromDescriptor(nestedMessageDescriptor(3), "proto3");
}, "should load descriptor message nesting up to the nesting limit");
test.throws(function() {
protobuf.Type.fromDescriptor(nestedMessageDescriptor(4), "proto3");
}, /max depth exceeded/, "should reject excessively nested descriptor messages");
protobuf.util.recursionLimit = 3;
protobuf.util.nestingLimit = 100;
test.doesNotThrow(function() {
protobuf.Root.fromDescriptor(packageDescriptor(3));
}, "should load descriptor package paths up to the recursion limit");
test.throws(function() {
protobuf.Root.fromDescriptor(packageDescriptor(4));
}, /max depth exceeded/, "should reject excessively nested descriptor package paths");
} finally {
protobuf.util.recursionLimit = recursionLimit;
protobuf.util.nestingLimit = nestingLimit;
}
test.end();
});
tape.test("descriptor - proto3 optional field without options", function(test) {
var fieldDescriptor = descriptor.FieldDescriptorProto.create({
name: "value",
number: 1,
label: 1,
type: 5,
proto3Optional: true
});
test.equal(protobuf.Field.fromDescriptor(fieldDescriptor, "proto3").options.proto3_optional, true, "accepts proto3 optional without field options");
test.end();
});
tape.test("descriptor - oneof and enum value options", function(test) {
var fieldPresence = descriptor.FeatureSet.lookup("FieldPresence").values.EXPLICIT,
oneofDescriptor = descriptor.OneofDescriptorProto.create({
name: "choice",
options: {
features: {
fieldPresence: fieldPresence
}
}
}),
oneof = protobuf.OneOf.fromDescriptor(oneofDescriptor),
enumDescriptor = descriptor.EnumDescriptorProto.create({
name: "State",
value: [{
name: "ON",
number: 0,
options: {
deprecated: true,
features: {
fieldPresence: fieldPresence
}
}
}]
}),
enm = protobuf.Enum.fromDescriptor(enumDescriptor),
unsafeEnumDescriptor = descriptor.EnumDescriptorProto.create({
name: "Unsafe",
value: [{
name: "__proto__",
number: 0,
options: {
deprecated: true
}
}, {
name: "OK",
number: 1,
options: {
deprecated: false
}
}]
}),
unsafe = protobuf.Enum.fromDescriptor(unsafeEnumDescriptor);
test.same(oneof.options, { features: { field_presence: "EXPLICIT" } }, "preserves oneof options");
test.equal(oneof.toDescriptor().options.features.fieldPresence, fieldPresence, "emits oneof options");
test.same(enm.valuesOptions.ON, { deprecated: true, features: { field_presence: "EXPLICIT" } }, "preserves enum value options");
test.equal(enm.toDescriptor().value[0].options.deprecated, true, "emits enum value options");
test.equal(enm.toDescriptor().value[0].options.features.fieldPresence, fieldPresence, "emits enum value features");
test.equal(unsafe.values.OK, 1, "keeps safe enum value");
test.equal(unsafe.valuesById[1], "OK", "keeps safe enum value by id");
test.notOk(Object.prototype.hasOwnProperty.call(unsafe.values, "__proto__"), "skips unsafe enum value name");
test.equal(unsafe.valuesOptions.deprecated, undefined, "does not expose unsafe enum value options through prototype");
test.same(unsafe.valuesOptions.OK, { deprecated: false }, "keeps safe enum value options");
test.end();
});
tape.test("descriptor - map field roundtrip", function(test) {
var root = protobuf.parse(`syntax = "proto3";
message Child {
string value = 1;
}
message Message {
map<string, int32> numbers = 1;
map<string, Child> children = 2;
}
`).root.resolveAll();
var Message = root.lookupType("Message"),
bytes = Message.encode(Message.create({
numbers: { a: 1 },
children: { b: { value: "x" } }
})).finish(),
descriptorSet = descriptor.FileDescriptorSet.decode(descriptor.FileDescriptorSet.encode(root.toDescriptor("proto3")).finish()),
root2 = protobuf.Root.fromDescriptor(descriptorSet).resolveAll(),
Message2 = root2.lookupType("Message");
test.ok(Message2.fields.numbers instanceof protobuf.MapField, "reconstructs scalar map fields");
test.ok(Message2.fields.children instanceof protobuf.MapField, "reconstructs message map fields");
test.notOk(Message2.nested && Message2.nested.Numbers, "does not expose synthetic map entry types");
test.same(Message2.toObject(Message2.decode(bytes)), {
numbers: { a: 1 },
children: { b: { value: "x" } }
}, "decodes descriptor maps as map objects");
test.end();
});
tape.test("descriptor - protoc map entry", function(test) {
var root = protobuf.Root.fromDescriptor(descriptor.FileDescriptorSet.create({
file: [{
name: "maps.proto",
package: "pkg",
syntax: "proto3",
messageType: [{
name: "Child",
field: [{
name: "value",
number: 1,
label: 1,
type: 9
}]
}, {
name: "Message",
field: [{
name: "numbers",
number: 1,
label: 3,
type: 11,
typeName: ".pkg.Message.NumbersEntry"
}, {
name: "children",
number: 2,
label: 3,
type: 11,
typeName: ".pkg.Message.ChildrenEntry"
}],
nestedType: [{
name: "NumbersEntry",
field: [
{ name: "key", number: 1, label: 1, type: 9 },
{ name: "value", number: 2, label: 1, type: 5 }
],
options: { mapEntry: true }
}, {
name: "ChildrenEntry",
field: [
{ name: "key", number: 1, label: 1, type: 9 },
{ name: "value", number: 2, label: 1, type: 11, typeName: ".pkg.Child" }
],
options: { mapEntry: true }
}]
}]
}]
})).resolveAll();
var Message = root.lookupType("pkg.Message");
test.ok(Message.fields.numbers instanceof protobuf.MapField, "reconstructs protoc-style map entry types");
test.equal(Message.fields.numbers.keyType, "string", "keeps map key type");
test.equal(Message.fields.numbers.type, "int32", "keeps map value type");
test.ok(Message.fields.children instanceof protobuf.MapField, "reconstructs message-valued map entry types");
test.equal(Message.fields.children.resolvedType.fullName, ".pkg.Child", "resolves qualified map value type");
test.end();
});