mirror of
https://github.com/hanzoai/protobuf.js.git
synced 2026-09-24 17:02:08 +00:00
294 lines
13 KiB
JavaScript
294 lines
13 KiB
JavaScript
var tape = require("tape");
|
|
|
|
var protobuf = require("..");
|
|
var textformat = require("../ext/textformat");
|
|
|
|
var proto = "syntax = \"proto3\";\
|
|
message Child { string name = 1; }\
|
|
enum Kind { ZERO = 0; ONE = 1; }\
|
|
message Msg {\
|
|
int32 int32_field = 1;\
|
|
uint32 uint32_field = 2;\
|
|
int64 int64_field = 3;\
|
|
uint64 uint64_field = 4;\
|
|
double double_field = 5;\
|
|
float float_field = 6;\
|
|
bool bool_field = 7;\
|
|
string string_field = 8;\
|
|
bytes bytes_field = 9;\
|
|
repeated int32 repeated_int32 = 10;\
|
|
Child child = 11;\
|
|
repeated Child children = 12;\
|
|
map<string, int32> string_map = 13;\
|
|
Kind kind = 14;\
|
|
oneof pick { string first = 15; string second = 16; }\
|
|
reserved \"old_field\";\
|
|
}";
|
|
|
|
var root = protobuf.parse(proto).root,
|
|
Msg = root.lookupType("Msg"),
|
|
Child = root.lookupType("Child"),
|
|
Kind = root.lookupEnum("Kind");
|
|
|
|
tape.test("textformat - installs reflected Type convenience methods", function(test) {
|
|
delete protobuf.Type.prototype.fromText;
|
|
delete protobuf.Type.prototype.toText;
|
|
|
|
textformat.install();
|
|
test.equal(typeof protobuf.Type.prototype.fromText, "function", "installs fromText");
|
|
test.equal(typeof protobuf.Type.prototype.toText, "function", "installs toText");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses scalar, repeated, map and nested fields", function(test) {
|
|
var msg = Msg.fromText(
|
|
"int32_field: - 052\n" +
|
|
"uint32_field: 0x2a\n" +
|
|
"int64_field: - 9007199254740991\n" +
|
|
"uint64_field: 18446744073709551615\n" +
|
|
"double_field: - inf\n" +
|
|
"float_field: nan\n" +
|
|
"bool_field: t\n" +
|
|
"string_field: \"hello\" \" \" \"\\303\\274\"\n" +
|
|
"bytes_field: \"\\000\\xff\\n\"\n" +
|
|
"repeated_int32: [1, 2]\n" +
|
|
"repeated_int32: 3\n" +
|
|
"child { name: \"kid\" }\n" +
|
|
"children: [{ name: \"a\" }, { name: \"b\" }]\n" +
|
|
"string_map { key: \"b\" value: 2 }\n" +
|
|
"string_map { key: \"a\" value: 1 }\n" +
|
|
"kind: ONE\n" +
|
|
"first: \"choice\"\n" +
|
|
"old_field: \"ignored\"\n"
|
|
);
|
|
|
|
test.equal(msg.int32Field, -42, "parses signed octal int32");
|
|
test.equal(msg.uint32Field, 42, "parses unsigned hex uint32");
|
|
test.equal(msg.int64Field.toString(), "-9007199254740991", "parses int64");
|
|
test.equal(msg.uint64Field.toString(), "18446744073709551615", "parses uint64");
|
|
test.equal(msg.doubleField, -Infinity, "parses signed infinity");
|
|
test.ok(isNaN(msg.floatField), "parses nan");
|
|
test.equal(msg.boolField, true, "parses bool shorthand");
|
|
test.equal(msg.stringField, "hello \u00fc", "parses concatenated utf8 strings");
|
|
test.same(Array.prototype.slice.call(msg.bytesField), [0, 255, 10], "parses bytes");
|
|
test.same(msg.repeatedInt32, [1, 2, 3], "parses repeated list and repeated field occurrences");
|
|
test.equal(msg.child.name, "kid", "parses nested message");
|
|
test.same(msg.children.map(function(child) { return child.name; }), ["a", "b"], "parses repeated message list");
|
|
test.same(msg.stringMap, { a: 1, b: 2 }, "parses map entries");
|
|
test.equal(msg.kind, Kind.values.ONE, "parses enum names");
|
|
test.equal(msg.first, "choice", "parses oneof member");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - preserves __proto__ map key as own data", function(test) {
|
|
var MapRoot = protobuf.parse("syntax = \"proto3\"; message Value { bool admin = 1; string role = 2; } message M { map<string, Value> labels = 1; }").root,
|
|
M = MapRoot.lookupType("M"),
|
|
msg = M.fromText("labels { key: \"__proto__\" value { admin: true role: \"owner\" } }");
|
|
|
|
test.same(Object.keys(msg.labels), ["__proto__"], "stores __proto__ as enumerable map key");
|
|
test.ok(Object.prototype.hasOwnProperty.call(msg.labels, "__proto__"), "stores __proto__ as own property");
|
|
test.equal(Object.getPrototypeOf(msg.labels), Object.prototype, "does not change map prototype");
|
|
test.equal("admin" in msg.labels, false, "does not expose value fields through map prototype");
|
|
test.equal(msg.labels.__proto__.admin, true, "keeps map value accessible by key");
|
|
test.equal(msg.labels.__proto__.role, "owner", "keeps complete map value");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses fields whose names shadow object prototype", function(test) {
|
|
var ShadowRoot = protobuf.parse("syntax = \"proto3\"; message M { repeated string to_string = 1; map<string, string> has_own_property = 2; }").root,
|
|
M = ShadowRoot.lookupType("M"),
|
|
msg = M.fromText("to_string: \"x\" has_own_property { key: \"a\" value: \"b\" }");
|
|
|
|
test.same(msg.toString, ["x"], "parses repeated field named toString");
|
|
test.same(msg.hasOwnProperty, { a: "b" }, "parses map field named hasOwnProperty");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - rejects inherited object properties as enum names", function(test) {
|
|
var EnumRoot = protobuf.parse("syntax = \"proto3\"; enum E { A = 0; } message M { E e = 1; }").root,
|
|
M = EnumRoot.lookupType("M");
|
|
|
|
test.throws(function() {
|
|
M.fromText("e: constructor");
|
|
}, /enum value expected/, "does not treat inherited constructor as an enum value");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - formats deterministically", function(test) {
|
|
var text = Msg.toText(Msg.create({
|
|
stringMap: { b: 2, a: 1 },
|
|
child: Child.create({ name: "kid" }),
|
|
repeatedInt32: [2, 1],
|
|
stringField: "a\nb",
|
|
bytesField: protobuf.util.newBuffer([0, 10, 255]),
|
|
kind: Kind.values.ONE
|
|
}));
|
|
|
|
test.equal(text, [
|
|
"string_field: \"a\\nb\"",
|
|
"bytes_field: \"\\000\\n\\377\"",
|
|
"repeated_int32: 2",
|
|
"repeated_int32: 1",
|
|
"child {",
|
|
" name: \"kid\"",
|
|
"}",
|
|
"string_map {",
|
|
" key: \"a\"",
|
|
" value: 1",
|
|
"}",
|
|
"string_map {",
|
|
" key: \"b\"",
|
|
" value: 2",
|
|
"}",
|
|
"kind: ONE"
|
|
].join("\n"), "formats in field-number order with sorted map keys");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - rejects duplicate singular and oneof fields", function(test) {
|
|
test.throws(function() {
|
|
Msg.fromText("string_field: \"a\" string_field: \"b\"");
|
|
}, /multiple values/, "rejects duplicate singular fields");
|
|
test.throws(function() {
|
|
Msg.fromText("first: \"a\" second: \"b\"");
|
|
}, /multiple values/, "rejects multiple oneof fields");
|
|
test.throws(function() {
|
|
Msg.fromText("unknown_field: 1");
|
|
}, /unknown field/, "rejects unknown fields");
|
|
test.throws(function() {
|
|
Msg.fromText("string_field: \"\\xff\"");
|
|
}, /invalid UTF-8/, "rejects invalid UTF-8 strings");
|
|
test.throws(function() {
|
|
Msg.fromText("repeated_int32: 1,, repeated_int32: 2");
|
|
}, /unexpected separator/, "rejects duplicate separators");
|
|
test.throws(function() {
|
|
Msg.fromText("bytes_field: \"\\ud800\"");
|
|
}, /illegal unicode escape/, "rejects surrogate unicode escapes");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses groups", function(test) {
|
|
var GroupRoot = protobuf.parse("syntax = \"proto2\"; message WithGroup { optional group MyGroup = 1 { optional int32 group_value = 2; } }").root,
|
|
WithGroup = GroupRoot.lookupType("WithGroup"),
|
|
msg = WithGroup.fromText("MyGroup { group_value: 7 }");
|
|
test.equal(msg.myGroup.groupValue, 7, "parses group field by group name");
|
|
test.equal(WithGroup.toText(msg), "MyGroup {\n group_value: 7\n}", "formats group field by group name");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses extensions", function(test) {
|
|
var ExtRoot = protobuf.parse("syntax = \"proto2\"; package pkg; message Base { extensions 100 to max; } extend Base { optional string ext_field = 100; }").root,
|
|
Base = ExtRoot.lookupType("pkg.Base"),
|
|
msg = Base.fromText("[pkg.ext_field]: \"x\"");
|
|
test.equal(msg[".pkg.extField"], "x", "parses extension field");
|
|
test.equal(Base.toText(msg), "[pkg.ext_field]: \"x\"", "formats extension field with proto-style name");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses group extensions by field name", function(test) {
|
|
var ExtRoot = protobuf.parse("syntax = \"proto2\"; package pkg; message Base { extensions 100 to max; } extend Base { optional group GroupField = 100 { optional int32 group_value = 1; } }").root,
|
|
Base = ExtRoot.lookupType("pkg.Base"),
|
|
msg = Base.fromText("[pkg.groupfield] { group_value: 1 }");
|
|
test.equal(msg[".pkg.groupField"].groupValue, 1, "parses group extension field name");
|
|
test.equal(Base.toText(msg), "[pkg.groupfield] {\n group_value: 1\n}", "formats group extension field name");
|
|
test.throws(function() {
|
|
Base.fromText("[pkg.GroupField] { group_value: 1 }");
|
|
}, /unknown extension/, "rejects group type name as extension field name");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - parses Any expansion", function(test) {
|
|
var AnyRoot = protobuf.parse("syntax = \"proto3\"; package google.protobuf; message Any { string type_url = 1; bytes value = 2; }").root;
|
|
protobuf.parse("syntax = \"proto3\"; message Payload { string value = 1; } message Wrap { google.protobuf.Any any = 1; }", AnyRoot);
|
|
var
|
|
Wrap = AnyRoot.lookupType("Wrap"),
|
|
Payload = AnyRoot.lookupType("Payload"),
|
|
msg = Wrap.fromText("any { [type.googleapis.com/Payload] { value: \"x\" } }"),
|
|
payload = Payload.decode(msg.any.value);
|
|
test.equal(msg.any.typeUrl, "type.googleapis.com/Payload", "sets Any type_url");
|
|
test.equal(payload.value, "x", "encodes expanded Any payload");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - preserves explicit defaults", function(test) {
|
|
var OpenRoot = protobuf.parse("syntax = \"proto3\"; enum E { A = 0; } message M { float value = 1; E choice = 2; }").root,
|
|
M = OpenRoot.lookupType("M"),
|
|
msg = M.fromText("value: -0 choice: 42");
|
|
test.equal(1 / msg.value, -Infinity, "preserves explicit negative zero");
|
|
test.equal(msg.choice, 42, "preserves unknown open enum number");
|
|
test.equal(M.toText(msg), "value: -0\nchoice: 42", "formats explicit default and open enum number");
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - enforces recursion limit", function(test) {
|
|
var RecRoot = protobuf.parse("syntax = \"proto3\"; message Node { Node child = 1; reserved \"old\"; }").root,
|
|
Node = RecRoot.lookupType("Node"),
|
|
limit = protobuf.util.recursionLimit;
|
|
protobuf.util.recursionLimit = 1;
|
|
try {
|
|
test.doesNotThrow(function() {
|
|
Node.fromText("child {}");
|
|
}, "parses nesting at limit");
|
|
test.throws(function() {
|
|
Node.fromText("child { child {} }");
|
|
}, /max depth exceeded/, "rejects parse nesting over limit");
|
|
test.throws(function() {
|
|
Node.fromText("old { old {} }");
|
|
}, /max depth exceeded/, "rejects reserved value nesting over limit");
|
|
test.doesNotThrow(function() {
|
|
Node.toText({ child: {} });
|
|
}, "formats nesting at limit");
|
|
test.throws(function() {
|
|
Node.toText({ child: { child: {} } });
|
|
}, /max depth exceeded/, "rejects format nesting over limit");
|
|
} finally {
|
|
protobuf.util.recursionLimit = limit;
|
|
}
|
|
test.end();
|
|
});
|
|
|
|
tape.test("textformat - optionally formats unknown fields", function(test) {
|
|
var UnknownRoot = protobuf.parse("syntax = \"proto3\"; message M { int32 known = 1; }").root,
|
|
M = UnknownRoot.lookupType("M"),
|
|
nested = protobuf.Writer.create().uint32(8).uint32(111).finish(),
|
|
bytes = protobuf.util.newBuffer([0, 255]),
|
|
encoded = protobuf.Writer.create()
|
|
.uint32(1001 << 3 | 0).uint32(123)
|
|
.uint32(1002 << 3 | 2).bytes(nested)
|
|
.uint32(1003 << 3 | 2).bytes(bytes)
|
|
.uint32(1004 << 3 | 5).fixed32(0x12345678)
|
|
.uint32(1005 << 3 | 1).raw([0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12])
|
|
.uint32(1006 << 3 | 3).uint32(8).uint32(321).uint32(1006 << 3 | 4)
|
|
.finish(),
|
|
reader = protobuf.Reader.create(encoded);
|
|
|
|
reader.discardUnknown = false;
|
|
var msg = M.decode(reader);
|
|
|
|
test.equal(M.toText(msg), "", "omits unknown fields by default");
|
|
test.equal(M.toText(msg, { unknowns: true }), [
|
|
"1001: 123",
|
|
"1002 {",
|
|
" 1: 111",
|
|
"}",
|
|
"1003: \"\\000\\377\"",
|
|
"1004: 0x12345678",
|
|
"1005: 0x123456789abcdef0",
|
|
"1006 {",
|
|
" 1: 321",
|
|
"}"
|
|
].join("\n"), "formats unknown fields with numeric text format syntax");
|
|
|
|
var unknownLimit = protobuf.textformat.unknownRecursionLimit,
|
|
nestedReader = protobuf.Reader.create(protobuf.Writer.create().uint32(1002 << 3 | 2).bytes(nested).finish());
|
|
nestedReader.discardUnknown = false;
|
|
var nestedOnly = M.decode(nestedReader);
|
|
protobuf.textformat.unknownRecursionLimit = 0;
|
|
try {
|
|
test.equal(M.toText(nestedOnly, { unknowns: true }), "1002: \"\\010o\"", "unknown recursion limit disables nested heuristic");
|
|
} finally {
|
|
protobuf.textformat.unknownRecursionLimit = unknownLimit;
|
|
}
|
|
test.end();
|
|
});
|