mirror of
https://github.com/hanzoai/protobuf.js.git
synced 2026-09-24 17:02:08 +00:00
834 lines
32 KiB
JavaScript
834 lines
32 KiB
JavaScript
var tape = require("tape");
|
|
|
|
var protobuf = require("..");
|
|
|
|
var def = {
|
|
fields: {}
|
|
};
|
|
|
|
var def2 = {
|
|
fields: {
|
|
a: {
|
|
type: "uint32",
|
|
id: 1
|
|
}
|
|
},
|
|
oneofs: {
|
|
kind: {
|
|
oneof: ["a"]
|
|
}
|
|
},
|
|
extensions: [[1000, 2000]],
|
|
reserved: [[900, 999], "b"],
|
|
nested: {
|
|
Type: {
|
|
values: { ONE: 1, TWO: 2 }
|
|
},
|
|
Service: {
|
|
methods: {}
|
|
}
|
|
},
|
|
options: {
|
|
custom: true
|
|
}
|
|
};
|
|
|
|
tape.test("reflected types", function(test) {
|
|
|
|
var type = protobuf.Type.fromJSON("Test", def);
|
|
test.same(type.toJSON(), def, "should construct from and convert back to JSON");
|
|
type = protobuf.Type.fromJSON("Test", def2);
|
|
test.same(JSON.parse(JSON.stringify(type)), JSON.parse(JSON.stringify(def2)), "should construct from and convert back to JSON (complex parsed)");
|
|
|
|
function MyMessageAuto() {}
|
|
type.ctor = MyMessageAuto;
|
|
test.ok(MyMessageAuto.prototype instanceof protobuf.Message, "should properly register a constructor through assignment");
|
|
test.ok(typeof MyMessageAuto.encode === "function", "should populate static methods on assigned constructors");
|
|
test.equal(new MyMessageAuto().a, 0, "should initialize prototype defaults on assigned constructors");
|
|
|
|
function MyMessageManual() {}
|
|
MyMessageManual.prototype = Object.create(protobuf.Message.prototype);
|
|
type.ctor = MyMessageManual;
|
|
test.ok(MyMessageManual.prototype instanceof protobuf.Message, "should properly register a constructor through assignment if already extending message");
|
|
test.ok(typeof MyMessageManual.encode === "function", "should populate static methods on assigned constructors");
|
|
test.equal(new MyMessageManual().a, 0, "should initialize prototype defaults on assigned constructors if already extending message");
|
|
|
|
function MyDecodeMessage() {}
|
|
type = protobuf.Type.fromJSON("DecodeTest", {
|
|
fields: {
|
|
a: { type: "uint32", id: 1 }
|
|
}
|
|
});
|
|
var buf = protobuf.Writer.create().uint32(8).uint32(42).finish();
|
|
test.notOk(type.decode(buf) instanceof MyDecodeMessage, "should decode with the generated constructor before assignment");
|
|
test.notOk(type.fromObject({ a: 42 }) instanceof MyDecodeMessage, "should convert with the generated constructor before assignment");
|
|
type.ctor = MyDecodeMessage;
|
|
test.ok(type.decode(buf) instanceof MyDecodeMessage, "should decode with assigned constructor after setup");
|
|
test.ok(type.fromObject({ a: 42 }) instanceof MyDecodeMessage, "should convert with assigned constructor after setup");
|
|
|
|
type = protobuf.Type.fromJSON("My", {
|
|
fields: {
|
|
a: {
|
|
type: "string",
|
|
id: 1
|
|
}
|
|
},
|
|
reserved: [[900, 999], "b"],
|
|
nested: {
|
|
Type: { fields: {} },
|
|
Enum: { values: {} },
|
|
Service: { methods: {} },
|
|
extensionField: { type: "string", id: 1000, extend: "Message" },
|
|
Other: { nested: {} }
|
|
}
|
|
});
|
|
test.same(type.toJSON(), {
|
|
fields: {
|
|
a: { id: 1, type: "string" }
|
|
},
|
|
reserved: [[900, 999], "b"],
|
|
nested: {
|
|
Type: { fields: {} },
|
|
Enum: { values: {} },
|
|
Service: { methods: {} },
|
|
extensionField: { extend: "Message", id: 1000, type: "string" },
|
|
Other: { }
|
|
}
|
|
}, "should create from Field, Type, Enum, Service, extension Field and Namespace JSON");
|
|
|
|
test.throws(function() {
|
|
type.add(new protobuf.Enum("Enum"));
|
|
}, Error, "should throw when trying to add duplicate names");
|
|
|
|
test.throws(function() {
|
|
type.add(new protobuf.Field("c", 1, "uint32"));
|
|
}, Error, "should throw when trying to add duplicate ids");
|
|
|
|
test.throws(function() {
|
|
type.add(new protobuf.Field("c", 900, "uint32"));
|
|
}, Error, "should throw when trying to add reserved ids");
|
|
|
|
test.throws(function() {
|
|
type.add(new protobuf.Field("c", 999, "uint32"));
|
|
}, Error, "should throw when trying to add reserved range end ids");
|
|
|
|
test.throws(function() {
|
|
type.add(new protobuf.Field("b", 2, "uint32"));
|
|
}, Error, "should throw when trying to add reserved names");
|
|
|
|
var renameType = new protobuf.Type("Rename");
|
|
var field = new protobuf.Field("before", 1, "string");
|
|
var oneof = new protobuf.OneOf("choice");
|
|
renameType.add(field);
|
|
renameType.add(oneof);
|
|
field.name = "after";
|
|
oneof.name = "selection";
|
|
renameType.remove(field);
|
|
renameType.remove(oneof);
|
|
test.equal(field.parent, null, "should remove renamed fields");
|
|
test.equal(oneof.parent, null, "should remove renamed oneofs");
|
|
test.same(renameType.fields, {}, "should remove renamed fields from the original key");
|
|
test.same(renameType.oneofs, {}, "should remove renamed oneofs from the original key");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("generated message constructors", function(test) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Message: {
|
|
fields: {
|
|
value: { type: "uint32", id: 1 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Message = root.lookupType("Message");
|
|
var msg = new Message.ctor(JSON.parse("{\"__proto__\":{\"marker\":true},\"value\":1}"));
|
|
|
|
test.equal(msg.value, 1, "should copy regular properties");
|
|
test.equal(msg.marker, undefined, "should ignore reserved properties");
|
|
|
|
var type = new protobuf.Type("Type");
|
|
type.add(new protobuf.Field("__proto__", 2, "uint32"));
|
|
test.equal(type.get("__proto__"), null, "should ignore reserved field names");
|
|
type.add(new protobuf.OneOf("__proto__"));
|
|
test.equal(type.get("__proto__"), null, "should ignore reserved oneof names");
|
|
|
|
["1Message", "default"].forEach(function(name) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
[name]: {
|
|
fields: {
|
|
value: { type: "uint32", id: 1 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Type = root.lookupType(name);
|
|
test.equal(Type.create({ value: 1 }).value, 1, "should create messages with generated-safe type names");
|
|
});
|
|
|
|
var ObjectMessage = protobuf.parse("syntax = \"proto3\"; message Object { string value = 1; }").root.lookupType("Object");
|
|
test.equal(ObjectMessage.create({ value: "x" }).value, "x", "should create messages with runtime-significant type names");
|
|
|
|
test.throws(function() {
|
|
protobuf.Root.fromJSON({
|
|
nested: {
|
|
TypeShadow: {
|
|
fields: {
|
|
$type: { type: "string", id: 1 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, /name '\$type' is reserved/, "should reject runtime-reserved field names");
|
|
|
|
test.throws(function() {
|
|
protobuf.Root.fromJSON({
|
|
nested: {
|
|
TypeShadow: {
|
|
oneofs: {
|
|
$type: { oneof: [] }
|
|
},
|
|
fields: {}
|
|
}
|
|
}
|
|
});
|
|
}, /name '\$type' is reserved/, "should reject runtime-reserved oneof names");
|
|
|
|
var ConstructorShadow = protobuf.parse("syntax = \"proto3\"; message ConstructorShadow { string constructor = 1; }").root.lookupType("ConstructorShadow");
|
|
test.equal(JSON.stringify(ConstructorShadow.decode(ConstructorShadow.encode({ constructor: "x" }).finish())), "{\"constructor\":\"x\"}", "should stringify messages with an own constructor field");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("decode nesting", function(test) {
|
|
function varint(value) {
|
|
var bytes = [];
|
|
do {
|
|
var b = value & 0x7F;
|
|
value >>>= 7;
|
|
if (value)
|
|
b |= 0x80;
|
|
bytes.push(b);
|
|
} while (value);
|
|
return bytes;
|
|
}
|
|
|
|
function nestedPayload(depth) {
|
|
var payload = [ 0x10, 0x2A ];
|
|
for (var i = 0; i < depth; ++i)
|
|
payload = [ 0x0A ].concat(varint(payload.length), payload);
|
|
return protobuf.util.newBuffer(payload);
|
|
}
|
|
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Node: {
|
|
fields: {
|
|
child: { type: "Node", id: 1 },
|
|
value: { type: "int32", id: 2 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Node = root.lookupType("Node");
|
|
var recursionLimit = protobuf.Reader.recursionLimit;
|
|
|
|
protobuf.Reader.recursionLimit = 3;
|
|
try {
|
|
test.equal(Node.decode(nestedPayload(2)).child.child.value, 42, "should decode below the limit");
|
|
test.throws(function() {
|
|
Node.decode(nestedPayload(4));
|
|
}, /max depth exceeded/, "should reject excessive nesting");
|
|
} finally {
|
|
protobuf.Reader.recursionLimit = recursionLimit;
|
|
}
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("encode nesting", function(test) {
|
|
function nestedObject(depth, field) {
|
|
var object = { value: 42 };
|
|
for (var i = 0; i < depth; ++i) {
|
|
if (field === "child")
|
|
object = { child: object };
|
|
else if (field === "children")
|
|
object = { children: [ object ] };
|
|
else
|
|
object = { childMap: { child: object } };
|
|
}
|
|
return object;
|
|
}
|
|
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Node: {
|
|
fields: {
|
|
child: { type: "Node", id: 1 },
|
|
children: { rule: "repeated", type: "Node", id: 2 },
|
|
childMap: { keyType: "string", type: "Node", id: 3 },
|
|
value: { type: "int32", id: 4 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Node = root.lookupType("Node");
|
|
var recursionLimit = protobuf.util.recursionLimit;
|
|
|
|
protobuf.util.recursionLimit = 3;
|
|
try {
|
|
test.ok(Node.encode(nestedObject(2, "child")).finish().length, "should encode singular messages below the limit");
|
|
test.throws(function() {
|
|
Node.encode(nestedObject(4, "child")).finish();
|
|
}, /max depth exceeded/, "should reject excessive singular message nesting");
|
|
|
|
test.ok(Node.encode(nestedObject(2, "children")).finish().length, "should encode repeated messages below the limit");
|
|
test.throws(function() {
|
|
Node.encode(nestedObject(4, "children")).finish();
|
|
}, /max depth exceeded/, "should reject excessive repeated message nesting");
|
|
|
|
test.ok(Node.encode(nestedObject(2, "childMap")).finish().length, "should encode map message values below the limit");
|
|
test.throws(function() {
|
|
Node.encode(nestedObject(4, "childMap")).finish();
|
|
}, /max depth exceeded/, "should reject excessive map message value nesting");
|
|
} finally {
|
|
protobuf.util.recursionLimit = recursionLimit;
|
|
}
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("encode setup preserves nesting", function(test) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Parent: {
|
|
fields: {
|
|
child: { type: "Child", id: 1 }
|
|
}
|
|
},
|
|
Child: {
|
|
fields: {
|
|
next: { type: "Child", id: 1 },
|
|
value: { type: "int32", id: 2 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Parent = root.lookupType("Parent");
|
|
var recursionLimit = protobuf.util.recursionLimit;
|
|
|
|
protobuf.util.recursionLimit = 1;
|
|
try {
|
|
test.throws(function() {
|
|
Parent.encode({ child: { next: { value: 42 } } }).finish();
|
|
}, /max depth exceeded/, "should preserve depth through nested type setup");
|
|
} finally {
|
|
protobuf.util.recursionLimit = recursionLimit;
|
|
}
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("decode known fields by wire type", function(test) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Message: {
|
|
fields: {
|
|
value: { type: "int32", id: 1 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Message = root.lookupType("Message");
|
|
var field0Varint = [ 0, 0 ];
|
|
var field1WireType6 = [ 1 << 3 | 6, 0 ];
|
|
var field1WireType7 = [ 1 << 3 | 7, 0 ];
|
|
var field1Fixed64 = [ 1 << 3 | 1, 1, 2, 3, 4, 5, 6, 7, 8 ];
|
|
var field1Group = [ 1 << 3 | 3, 1 << 3 | 4 ];
|
|
var field1GroupField2End = [ 1 << 3 | 3, 2 << 3 | 4 ];
|
|
var field1TagWithContinuation = 1 << 3 | 128;
|
|
var fieldNumberTooHigh = [ field1TagWithContinuation, 128, 128, 128, 128, 15, 210, 9 ];
|
|
var fieldNumberSlightlyTooHigh = [ field1TagWithContinuation, 128, 128, 128, 64, 210, 9 ];
|
|
var overlongTagVarint = [ field1TagWithContinuation, 128, 128, 128, 128, 128, 128, 128, 0, 210, 9 ];
|
|
var tagVarintMoreThanTenBytes = [ field1TagWithContinuation, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0, 210, 9 ];
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(field0Varint));
|
|
}, /illegal tag: field number 0/, "should reject field number 0");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(field1WireType6));
|
|
}, /invalid wire type 6/, "should reject invalid wire types for known fields");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(field1WireType7));
|
|
}, /invalid wire type 7/, "should reject invalid wire types for known fields");
|
|
|
|
var message = Message.decode(protobuf.util.newBuffer(field1Fixed64));
|
|
test.notOk(Object.prototype.hasOwnProperty.call(message, "value"), "should skip valid but unexpected wire types");
|
|
|
|
message = Message.decode(protobuf.util.newBuffer(field1Group));
|
|
test.notOk(Object.prototype.hasOwnProperty.call(message, "value"), "should skip valid but unexpected groups");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(field1GroupField2End));
|
|
}, /invalid end group tag/, "should reject mismatched unknown group tags");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(fieldNumberTooHigh));
|
|
}, /invalid tag encoding/, "should reject tags above the maximum field number");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(fieldNumberSlightlyTooHigh));
|
|
}, /invalid tag encoding/, "should reject tags above uint32 range");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(overlongTagVarint));
|
|
}, /invalid tag encoding/, "should reject overlong tag varints");
|
|
|
|
test.throws(function() {
|
|
Message.decode(protobuf.util.newBuffer(tagVarintMoreThanTenBytes));
|
|
}, /invalid tag encoding/, "should reject tag varints longer than ten bytes");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("decode string fields by utf8_validation feature", function(test) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Verify: {
|
|
fields: {
|
|
singular: { type: "string", id: 1 },
|
|
repeated: { rule: "repeated", type: "string", id: 2 },
|
|
map: { keyType: "string", type: "string", id: 3 },
|
|
choice: { type: "string", id: 4 }
|
|
},
|
|
oneofs: {
|
|
kind: { oneof: [ "choice" ] }
|
|
}
|
|
},
|
|
Proto2: {
|
|
edition: "proto2",
|
|
fields: {
|
|
value: { type: "string", id: 1 }
|
|
}
|
|
},
|
|
EditionNone: {
|
|
edition: "2023",
|
|
options: { features: { utf8_validation: "NONE" } },
|
|
fields: {
|
|
value: { type: "string", id: 1 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Verify = root.lookupType("Verify");
|
|
var Proto2 = root.lookupType("Proto2");
|
|
var EditionNone = root.lookupType("EditionNone");
|
|
var invalid = [ 0xA0, 0xB0, 0xC0, 0xD0 ];
|
|
var surrogate = [ 0xED, 0xA0, 0x80 ];
|
|
var valid = [ 0x66, 0x6F, 0x6F ];
|
|
var invalidReplacement = protobuf.util.newBuffer(invalid).toString("utf8");
|
|
var surrogateReplacement = protobuf.util.newBuffer(surrogate).toString("utf8");
|
|
|
|
function stringField(id, value) {
|
|
return [ id << 3 | 2, value.length ].concat(value);
|
|
}
|
|
|
|
function mapField(key, value) {
|
|
var entry = stringField(1, key).concat(stringField(2, value));
|
|
return [ 3 << 3 | 2, entry.length ].concat(entry);
|
|
}
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(stringField(1, invalid)));
|
|
}, /utf-?8|encoded data/i, "should reject invalid singular strings when verification is enabled");
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(stringField(2, invalid)));
|
|
}, /utf-?8|encoded data/i, "should reject invalid repeated strings when verification is enabled");
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(stringField(4, invalid)));
|
|
}, /utf-?8|encoded data/i, "should reject invalid oneof strings when verification is enabled");
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(mapField(invalid, valid)));
|
|
}, /utf-?8|encoded data/i, "should reject invalid map string keys when verification is enabled");
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(mapField(valid, invalid)));
|
|
}, /utf-?8|encoded data/i, "should reject invalid map string values when verification is enabled");
|
|
|
|
test.throws(function() {
|
|
Verify.decode(protobuf.util.newBuffer(stringField(1, surrogate)));
|
|
}, /utf-?8|encoded data/i, "should reject UTF8-encoded lone surrogates when verification is enabled");
|
|
|
|
test.equal(
|
|
Verify.decode(stringField(1, [ 0xC3, 0xBC ])).singular,
|
|
"\u00fc",
|
|
"should decode valid non-ASCII strings from array buffers when verification is enabled"
|
|
);
|
|
|
|
test.equal(
|
|
Proto2.decode(protobuf.util.newBuffer(stringField(1, invalid))).value,
|
|
invalidReplacement,
|
|
"should replace malformed strings when verification is disabled by proto2 defaults"
|
|
);
|
|
|
|
test.equal(
|
|
EditionNone.decode(protobuf.util.newBuffer(stringField(1, invalid))).value,
|
|
invalidReplacement,
|
|
"should replace malformed strings when verification is disabled by editions features"
|
|
);
|
|
|
|
test.equal(
|
|
Proto2.decode(protobuf.util.newBuffer(stringField(1, surrogate))).value,
|
|
surrogateReplacement,
|
|
"should replace UTF8-encoded lone surrogates when verification is disabled by proto2 defaults"
|
|
);
|
|
|
|
test.equal(
|
|
EditionNone.decode(protobuf.util.newBuffer(stringField(1, surrogate))).value,
|
|
surrogateReplacement,
|
|
"should replace UTF8-encoded lone surrogates when verification is disabled by editions features"
|
|
);
|
|
|
|
test.end();
|
|
});
|
|
|
|
// TODO: Protoc rejects open enums whose first value is not zero.
|
|
// We should do the same, but for v8 this would be a regression.
|
|
// Remove this test once we enforce this restriction.
|
|
tape.test("decode implicit enum zero with non-zero default", function(test) {
|
|
var Message = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Op: { values: { UNKNOWN: -1, INSERT: 0 } },
|
|
Message: {
|
|
fields: {
|
|
op: { type: "Op", id: 4 }
|
|
}
|
|
}
|
|
}
|
|
}).lookupType("Message");
|
|
|
|
test.notOk(Message.fields.op.hasPresence, "enum field should have implicit presence");
|
|
test.equal(Message.fields.op.typeDefault, -1, "enum field should use the first value as default");
|
|
test.equal(Message.decode(protobuf.util.newBuffer([0x20, 0x00])).op, 0, "should preserve explicit enum zero");
|
|
test.equal(Buffer.from(Message.encode({ op: -1 }).finish()).toString("hex"), "", "should omit implicit enum default");
|
|
test.equal(Buffer.from(Message.encode({ op: 0 }).finish()).toString("hex"), "2000", "should encode non-default enum zero");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("encode omits implicit default values", function(test) {
|
|
var Message = protobuf.parse("syntax=\"proto3\";\
|
|
message Message {\
|
|
int32 int32_value = 1;\
|
|
bool bool_value = 2;\
|
|
string string_value = 3;\
|
|
bytes bytes_value = 4;\
|
|
double double_value = 5;\
|
|
float float_value = 6;\
|
|
Enum enum_value = 7;\
|
|
optional int32 optional_int32_value = 8;\
|
|
optional double optional_double_value = 9;\
|
|
oneof kind { int32 oneof_int32_value = 10; }\
|
|
enum Enum { A = 0; B = 1; }\
|
|
}").root.resolveAll().lookupType("Message");
|
|
|
|
function hex(buf) {
|
|
return Buffer.from(buf).toString("hex");
|
|
}
|
|
|
|
test.equal(hex(Message.encode({
|
|
int32Value: 0,
|
|
boolValue: false,
|
|
stringValue: "",
|
|
bytesValue: [],
|
|
doubleValue: 0,
|
|
floatValue: 0,
|
|
enumValue: 0
|
|
}).finish()), "", "should omit implicit scalar defaults");
|
|
|
|
test.equal(hex(Message.encode({
|
|
doubleValue: -0,
|
|
floatValue: -0
|
|
}).finish()), "2900000000000000803500000080", "should preserve implicit negative zero");
|
|
|
|
test.equal(hex(Message.encode({
|
|
optionalInt32Value: 0,
|
|
optionalDoubleValue: -0,
|
|
oneofInt32Value: 0
|
|
}).finish()), "40004900000000000000805000", "should encode explicit defaults");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("decode preserves implicit negative zero", function(test) {
|
|
var Message = protobuf.parse("syntax=\"proto3\";\
|
|
message Message {\
|
|
double double_value = 1;\
|
|
float float_value = 2;\
|
|
optional double optional_double_value = 3;\
|
|
optional float optional_float_value = 4;\
|
|
}").root.resolveAll().lookupType("Message");
|
|
|
|
function bytes(hex) {
|
|
return protobuf.util.newBuffer(Buffer.from(hex, "hex"));
|
|
}
|
|
|
|
function hex(buf) {
|
|
return Buffer.from(buf).toString("hex");
|
|
}
|
|
|
|
var implicit = Message.decode(bytes("0900000000000000801500000080"));
|
|
test.ok(Object.is(implicit.doubleValue, -0), "should decode implicit double negative zero");
|
|
test.ok(Object.is(implicit.floatValue, -0), "should decode implicit float negative zero");
|
|
test.equal(hex(Message.encode(implicit).finish()), "0900000000000000801500000080", "should re-encode implicit negative zero");
|
|
|
|
var positive = Message.decode(bytes("0900000000000000001500000000"));
|
|
test.notOk(Object.prototype.hasOwnProperty.call(positive, "doubleValue"), "should drop implicit double positive zero");
|
|
test.notOk(Object.prototype.hasOwnProperty.call(positive, "floatValue"), "should drop implicit float positive zero");
|
|
test.equal(hex(Message.encode(positive).finish()), "", "should not re-encode implicit positive zero");
|
|
|
|
var explicit = Message.decode(bytes("1900000000000000802500000080"));
|
|
test.ok(Object.is(explicit.optionalDoubleValue, -0), "should decode explicit double negative zero");
|
|
test.ok(Object.is(explicit.optionalFloatValue, -0), "should decode explicit float negative zero");
|
|
test.equal(hex(Message.encode(explicit).finish()), "1900000000000000802500000080", "should re-encode explicit negative zero");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("fromObject preserves implicit negative zero", function(test) {
|
|
var Message = protobuf.parse("syntax=\"proto3\";\
|
|
message Message {\
|
|
double double_value = 1;\
|
|
float float_value = 2;\
|
|
optional double optional_double_value = 3;\
|
|
optional float optional_float_value = 4;\
|
|
}").root.resolveAll().lookupType("Message");
|
|
|
|
function hex(buf) {
|
|
return Buffer.from(buf).toString("hex");
|
|
}
|
|
|
|
var implicit = Message.fromObject({
|
|
doubleValue: -0,
|
|
floatValue: -0
|
|
});
|
|
test.ok(Object.is(implicit.doubleValue, -0), "should convert implicit double negative zero");
|
|
test.ok(Object.is(implicit.floatValue, -0), "should convert implicit float negative zero");
|
|
test.equal(hex(Message.encode(implicit).finish()), "0900000000000000801500000080", "should encode converted implicit negative zero");
|
|
|
|
var positive = Message.fromObject({
|
|
doubleValue: 0,
|
|
floatValue: 0
|
|
});
|
|
test.notOk(Object.prototype.hasOwnProperty.call(positive, "doubleValue"), "should drop implicit double positive zero");
|
|
test.notOk(Object.prototype.hasOwnProperty.call(positive, "floatValue"), "should drop implicit float positive zero");
|
|
test.equal(hex(Message.encode(positive).finish()), "", "should not encode converted implicit positive zero");
|
|
|
|
var explicit = Message.fromObject({
|
|
optionalDoubleValue: -0,
|
|
optionalFloatValue: -0
|
|
});
|
|
test.ok(Object.is(explicit.optionalDoubleValue, -0), "should convert explicit double negative zero");
|
|
test.ok(Object.is(explicit.optionalFloatValue, -0), "should convert explicit float negative zero");
|
|
test.equal(hex(Message.encode(explicit).finish()), "1900000000000000802500000080", "should encode converted explicit negative zero");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("object conversion nesting", function(test) {
|
|
function nestedObject(depth) {
|
|
var object = { value: 42 };
|
|
for (var i = 0; i < depth; ++i)
|
|
object = { child: object };
|
|
return object;
|
|
}
|
|
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Node: {
|
|
fields: {
|
|
child: { type: "Node", id: 1 },
|
|
value: { type: "int32", id: 2 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Node = root.lookupType("Node");
|
|
var recursionLimit = protobuf.util.recursionLimit;
|
|
|
|
protobuf.util.recursionLimit = 3;
|
|
try {
|
|
test.equal(Node.verify(nestedObject(2)), null, "should verify below the limit");
|
|
test.match(Node.verify(nestedObject(4)), /max depth exceeded/, "should reject excessive nesting while verifying");
|
|
test.equal(Node.fromObject(nestedObject(2)).child.child.value, 42, "should convert below the limit");
|
|
test.throws(function() {
|
|
Node.fromObject(nestedObject(4));
|
|
}, /max depth exceeded/, "should reject excessive nesting while converting");
|
|
} finally {
|
|
protobuf.util.recursionLimit = recursionLimit;
|
|
}
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("object conversion rejects null message values", function(test) {
|
|
var root = protobuf.Root.fromJSON({
|
|
nested: {
|
|
Document: {
|
|
fields: {
|
|
attrs: { keyType: "string", type: "Metadata", id: 1 },
|
|
tags: { rule: "repeated", type: "Metadata", id: 2 },
|
|
meta: { type: "Metadata", id: 3 }
|
|
}
|
|
},
|
|
Metadata: {
|
|
fields: {
|
|
key: { type: "string", id: 1 },
|
|
value: { type: "string", id: 2 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var Document = root.lookupType("Document");
|
|
var Metadata = root.lookupType("Metadata");
|
|
|
|
test.throws(function() {
|
|
Metadata.fromObject(null);
|
|
}, /object expected/, "should reject null top-level messages");
|
|
|
|
test.throws(function() {
|
|
Document.fromObject({ attrs: { bad: null } });
|
|
}, /object expected/, "should reject null map message values");
|
|
|
|
test.throws(function() {
|
|
Document.fromObject({ tags: [ null ] });
|
|
}, /object expected/, "should reject null repeated message values");
|
|
|
|
test.equal(Object.prototype.hasOwnProperty.call(Document.fromObject({ meta: null }), "meta"), false, "should ignore null singular message fields");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("feature resolution legacy proto3", function(test) {
|
|
var json = {
|
|
fields: {
|
|
regular: { type: "string", id: 1 },
|
|
packed: { type: "int32", id: 2, rule: "repeated" },
|
|
unpacked: { type: "int32", id: 3, rule: "repeated", options: { packed: false } }
|
|
},
|
|
nested: { Nested: { fields: {
|
|
regular: { type: "string", id: 1 },
|
|
packed: { type: "int32", id: 2, rule: "repeated" },
|
|
unpacked: { type: "int32", id: 3, rule: "repeated", options: { packed: false } }
|
|
} } }
|
|
};
|
|
var root = new protobuf.Root();
|
|
var Type = protobuf.Type.fromJSON("My", json);
|
|
root.add(Type).resolveAll();
|
|
|
|
var Nested = Type.nested.Nested;
|
|
|
|
test.same(Type.toJSON(), json, "JSON should roundtrip");
|
|
test.same(Nested.toJSON(), json.nested.Nested, "nested JSON should roundtrip");
|
|
|
|
test.equal(Type._edition, "proto3", "should infer proto3 syntax");
|
|
test.notOk(Type.fields.regular.hasPresence, "should have implicit presence by default");
|
|
test.ok(Type.fields.packed.packed, "should have packed encoding by default");
|
|
test.notOk(Type.fields.unpacked.packed, "should override expanded encoding");
|
|
|
|
test.equal(Nested._edition, null, "should not infer proto3 syntax");
|
|
test.notOk(Nested.fields.regular.hasPresence, "nested should have implicit presence by default");
|
|
test.ok(Nested.fields.packed.packed, "nested should have packed encoding by default");
|
|
test.notOk(Nested.fields.unpacked.packed, "nested should override expanded encoding");
|
|
|
|
test.end();
|
|
});
|
|
|
|
tape.test("feature resolution proto2", function(test) {
|
|
var json = {
|
|
edition: "proto2",
|
|
fields: {
|
|
regular: { type: "string", id: 1 },
|
|
required: { type: "string", id: 2, rule: "required" },
|
|
packed: { type: "int32", id: 3, rule: "repeated", options: { packed: true } },
|
|
unpacked: { type: "int32", id: 4, rule: "repeated"}
|
|
},
|
|
nested: { Nested: { fields: {
|
|
regular: { type: "string", id: 1 },
|
|
packed: { type: "int32", id: 2, rule: "repeated", options: { packed: true } },
|
|
unpacked: { type: "int32", id: 3, rule: "repeated" }
|
|
} } }
|
|
};
|
|
var root = new protobuf.Root();
|
|
var Type = protobuf.Type.fromJSON("My", json);
|
|
root.add(Type).resolveAll();
|
|
|
|
var Nested = Type.nested.Nested;
|
|
|
|
test.same(Type.toJSON(), json, "JSON should roundtrip");
|
|
test.same(Nested.toJSON(), json.nested.Nested, "nested JSON should roundtrip");
|
|
|
|
test.equal(Type._edition, "proto2", "should set edition");
|
|
test.ok(Type.fields.regular.hasPresence, "should have explicit presence by default");
|
|
test.ok(Type.fields.required.required, "should have required fields");
|
|
test.ok(Type.fields.packed.packed, "should override packed encoding");
|
|
test.notOk(Type.fields.unpacked.packed, "should have expanded encoding by default");
|
|
|
|
test.equal(Nested._edition, null, "should not set edition");
|
|
test.ok(Nested.fields.regular.hasPresence, "nested should have explicit presence by default");
|
|
test.notOk(Nested.fields.unpacked.packed, "nested should have expanded encoding by default");
|
|
test.ok(Nested.fields.packed.packed, "nested should override packed encoding");
|
|
|
|
test.end();
|
|
});
|
|
|
|
|
|
tape.test("feature resolution edition 2023", function(test) {
|
|
var json = {
|
|
edition: "2023",
|
|
fields: {
|
|
explicit: { type: "string", id: 1 },
|
|
implicit: { type: "string", id: 2, options: { "features": { "field_presence": "IMPLICIT" } } },
|
|
required: { type: "string", id: 3, rule: "required", options: { "features": { "field_presence": "LEGACY_REQUIRED" } } },
|
|
packed: { type: "int32", id: 4, rule: "repeated" },
|
|
unpacked: { type: "int32", id: 5, rule: "repeated", options: { "features": { "repeated_field_encoding": "EXPANDED" } } }
|
|
},
|
|
nested: { Nested: { fields: {
|
|
explicit: { type: "string", id: 1 },
|
|
implicit: { type: "string", id: 2, options: { "features": { "field_presence": "IMPLICIT" } } },
|
|
packed: { type: "int32", id: 3, rule: "repeated" },
|
|
unpacked: { type: "int32", id: 4, rule: "repeated", options: { "features": { "repeated_field_encoding": "EXPANDED" } } }
|
|
} } }
|
|
};
|
|
var root = new protobuf.Root();
|
|
var Type = protobuf.Type.fromJSON("My", json);
|
|
root.add(Type).resolveAll();
|
|
|
|
var Nested = Type.nested.Nested;
|
|
|
|
test.same(Type.toJSON(), json, "JSON should roundtrip");
|
|
test.same(Nested.toJSON(), json.nested.Nested, "nested JSON should roundtrip");
|
|
|
|
test.equal(Type._edition, "2023", "should set edition");
|
|
test.ok(Type.fields.explicit.hasPresence, "should have explicit presence");
|
|
test.notOk(Type.fields.implicit.hasPresence, "should have implicit presence");
|
|
test.ok(Type.fields.required.required, "should have required presence");
|
|
test.ok(Type.fields.packed.packed, "should have packed encoding");
|
|
test.notOk(Type.fields.unpacked.packed, "should have expanded encoding");
|
|
|
|
test.equal(Nested._edition, null, "should not set edition");
|
|
test.ok(Nested.fields.explicit.hasPresence, "nested should have explicit presence");
|
|
test.notOk(Nested.fields.implicit.hasPresence, "nested should have implicit presence");
|
|
test.ok(Nested.fields.packed.packed, "nested should have packed encoding");
|
|
test.notOk(Nested.fields.unpacked.packed, "nested should have expanded encoding");
|
|
|
|
test.end();
|
|
});
|