/* Copyright 2013 Daniel Wirtz Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * @license ProtoBuf.js (c) 2013 Daniel Wirtz * Released under the Apache License, Version 2.0 * see: https://github.com/dcodeIO/ProtoBuf.js for details */ (function(global) { "use strict"; function loadProtoBuf(ByteBuffer) { /** * The ProtoBuf namespace. * @exports ProtoBuf * @namespace * @expose */ var ProtoBuf = {}; /** * ProtoBuf.js version. * @type {string} * @const * @expose */ ProtoBuf.VERSION = "2.0.5"; /** * Wire types. * @type {Object.} * @const * @expose */ ProtoBuf.WIRE_TYPES = {}; /** * Varint wire type. * @type {number} * @expose */ ProtoBuf.WIRE_TYPES.VARINT = 0; /** * Fixed 64 bits wire type. * @type {number} * @const * @expose */ ProtoBuf.WIRE_TYPES.BITS64 = 1; /** * Length delimited wire type. * @type {number} * @const * @expose */ ProtoBuf.WIRE_TYPES.LDELIM = 2; /** * Start group wire type. * @type {number} * @const * @deprecated Not supported. * @expose */ ProtoBuf.WIRE_TYPES.STARTGROUP = 3; /** * End group wire type. * @type {number} * @const * @deprecated Not supported. * @expose */ ProtoBuf.WIRE_TYPES.ENDGROUP = 4; /** * Fixed 32 bits wire type. * @type {number} * @const * @expose */ ProtoBuf.WIRE_TYPES.BITS32 = 5; /** * Types. * @dict * @type {Object.} * @const * @expose */ ProtoBuf.TYPES = { // According to the protobuf spec. "int32": { name: "int32", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "uint32": { name: "uint32", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "sint32": { name: "sint32", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "int64": { name: "int64", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "uint64": { name: "uint64", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "sint64": { name: "sint64", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "bool": { name: "bool", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "double": { name: "double", wireType: ProtoBuf.WIRE_TYPES.BITS64 }, "string": { name: "string", wireType: ProtoBuf.WIRE_TYPES.LDELIM }, "bytes": { name: "bytes", wireType: ProtoBuf.WIRE_TYPES.LDELIM }, "fixed32": { name: "fixed32", wireType: ProtoBuf.WIRE_TYPES.BITS32 }, "sfixed32": { name: "sfixed32", wireType: ProtoBuf.WIRE_TYPES.BITS32 }, "fixed64": { name: "fixed64", wireType: ProtoBuf.WIRE_TYPES.BITS64 }, "sfixed64": { name: "sfixed64", wireType: ProtoBuf.WIRE_TYPES.BITS64 }, "float": { name: "float", wireType: ProtoBuf.WIRE_TYPES.BITS32 }, "enum": { name: "enum", wireType: ProtoBuf.WIRE_TYPES.VARINT }, "message": { name: "message", wireType: ProtoBuf.WIRE_TYPES.LDELIM } }; /** * @type {?Long} */ ProtoBuf.Long = ByteBuffer.Long; /** * If set to `true`, field names will be converted from underscore notation to camel case. Defaults to `false`. * Must be set prior to parsing. * @type {boolean} * @expose */ ProtoBuf.convertFieldsToCamelCase = false; /** * @alias ProtoBuf.Util * @expose */ ProtoBuf.Util = (function() { "use strict"; // Object.create polyfill // ref: https://developer.mozilla.org/de/docs/JavaScript/Reference/Global_Objects/Object/create if (!Object.create) { /** @expose */ Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation only accepts the first parameter.'); } function F() {} F.prototype = o; return new F(); }; } /** * ProtoBuf utilities. * @exports ProtoBuf.Util * @namespace */ var Util = {}; /** * Flag if running in node or not. * @type {boolean} * @const * @expose */ Util.IS_NODE = (typeof window === 'undefined' || !window.window) && typeof require === 'function' && typeof process !== 'undefined' && typeof process["nextTick"] === 'function'; /** * Constructs a XMLHttpRequest object. * @return {XMLHttpRequest} * @throws {Error} If XMLHttpRequest is not supported * @expose */ Util.XHR = function() { // No dependencies please, ref: http://www.quirksmode.org/js/xmlhttp.html var XMLHttpFactories = [ function () {return new XMLHttpRequest()}, function () {return new ActiveXObject("Msxml2.XMLHTTP")}, function () {return new ActiveXObject("Msxml3.XMLHTTP")}, function () {return new ActiveXObject("Microsoft.XMLHTTP")} ]; /** @type {?XMLHttpRequest} */ var xhr = null; for (var i=0;i} * @namespace * @expose */ var Lang = { // Look, so cute! OPEN: "{", CLOSE: "}", OPTOPEN: "[", OPTCLOSE: "]", OPTEND: ",", EQUAL: "=", END: ";", STRINGOPEN: '"', STRINGCLOSE: '"', COPTOPEN: '(', COPTCLOSE: ')', DELIM: /[\s\{\}=;\[\],"\(\)]/g, KEYWORD: /^(?:package|option|import|message|enum|extend|service|syntax|extensions)$/, RULE: /^(?:required|optional|repeated)$/, TYPE: /^(?:double|float|int32|uint32|sint32|int64|uint64|sint64|fixed32|sfixed32|fixed64|sfixed64|bool|string|bytes)$/, NAME: /^[a-zA-Z][a-zA-Z_0-9]*$/, OPTNAME: /^(?:[a-zA-Z][a-zA-Z_0-9]*|\([a-zA-Z][a-zA-Z_0-9]*\))$/, TYPEDEF: /^[a-zA-Z][a-zA-Z_0-9]*$/, TYPEREF: /^(?:\.?[a-zA-Z][a-zA-Z_0-9]*)+$/, FQTYPEREF: /^(?:\.[a-zA-Z][a-zA-Z_0-9]*)+$/, NUMBER: /^-?(?:[1-9][0-9]*|0|0x[0-9a-fA-F]+|0[0-7]+|[0-9]*\.[0-9]+)$/, NUMBER_DEC: /^(?:[1-9][0-9]*|0)$/, NUMBER_HEX: /^0x[0-9a-fA-F]+$/, NUMBER_OCT: /^0[0-7]+$/, NUMBER_FLT: /^[0-9]*\.[0-9]+$/, ID: /^(?:[1-9][0-9]*|0|0x[0-9a-fA-F]+|0[0-7]+)$/, NEGID: /^\-?(?:[1-9][0-9]*|0|0x[0-9a-fA-F]+|0[0-7]+)$/, WHITESPACE: /\s/, STRING: /"([^"\\]*(\\.[^"\\]*)*)"/g, BOOL: /^(?:true|false)$/i, ID_MIN: 1, ID_MAX: 0x1FFFFFFF }; return Lang; })(); /** * Utilities to parse .proto files. * @namespace * @expose */ ProtoBuf.DotProto = {}; // Not present in "noparse" builds /** * @alias ProtoBuf.DotProto.Tokenizer * @expose */ ProtoBuf.DotProto.Tokenizer = (function(Lang) { /** * Constructs a new Tokenizer. * @exports ProtoBuf.DotProto.Tokenizer * @class A ProtoBuf .proto Tokenizer. * @param {string} proto Proto to tokenize * @constructor */ var Tokenizer = function(proto) { /** * Source to parse. * @type {string} * @expose */ this.source = ""+proto; /** * Current index. * @type {number} * @expose */ this.index = 0; /** * Current line. * @type {number} * @expose */ this.line = 1; /** * Stacked values. * @type {Array} * @expose */ this.stack = []; /** * Whether currently reading a string or not. * @type {boolean} * @expose */ this.readingString = false; }; /** * Reads a string beginning at the current index. * @return {string} The string * @throws {Error} If it's not a valid string * @private */ Tokenizer.prototype._readString = function() { Lang.STRING.lastIndex = this.index-1; // Include the open quote var match; if ((match = Lang.STRING.exec(this.source)) !== null) { var s = match[1]; this.index = Lang.STRING.lastIndex; this.stack.push(Lang.STRINGCLOSE); return s; } throw(new Error("Illegal string value at line "+this.line+", index "+this.index)); }; /** * Gets the next token and advances by one. * @return {?string} Token or `null` on EOF * @throws {Error} If it's not a valid proto file * @expose */ Tokenizer.prototype.next = function() { if (this.stack.length > 0) { return this.stack.shift(); } if (this.index >= this.source.length) { return null; // No more tokens } if (this.readingString) { this.readingString = false; return this._readString(); } var repeat, last; do { repeat = false; // Strip white spaces while (Lang.WHITESPACE.test(last = this.source.charAt(this.index))) { this.index++; if (last === "\n") this.line++; if (this.index === this.source.length) return null; } // Strip comments if (this.source.charAt(this.index) === '/') { if (this.source.charAt(++this.index) === '/') { // Single line while (this.source.charAt(this.index) !== "\n") { this.index++; if (this.index == this.source.length) return null; } this.index++; this.line++; repeat = true; } else if (this.source.charAt(this.index) === '*') { /* Block */ last = ''; while (last+(last=this.source.charAt(this.index)) !== '*/') { this.index++; if (last === "\n") this.line++; if (this.index === this.source.length) return null; } this.index++; repeat = true; } else { throw(new Error("Invalid comment at line "+this.line+": /"+this.source.charAt(this.index)+" ('/' or '*' expected)")); } } } while (repeat); if (this.index === this.source.length) return null; // Read the next token var end = this.index; Lang.DELIM.lastIndex = 0; var delim = Lang.DELIM.test(this.source.charAt(end)); if (!delim) { end++; while(end < this.source.length && !Lang.DELIM.test(this.source.charAt(end))) { end++; } } else { end++; } var token = this.source.substring(this.index, this.index = end); if (token === Lang.STRINGOPEN) { this.readingString = true; } return token; }; /** * Peeks for the next token. * @return {?string} Token or `null` on EOF * @throws {Error} If it's not a valid proto file * @expose */ Tokenizer.prototype.peek = function() { if (this.stack.length == 0) { var token = this.next(); if (token === null) return null; this.stack.push(token); } return this.stack[0]; }; /** * Returns a string representation of this object. * @return {string} String representation as of "Tokenizer(index/length)" * @expose */ Tokenizer.prototype.toString = function() { return "Tokenizer("+this.index+"/"+this.source.length+" at line "+this.line+")"; }; return Tokenizer; })(ProtoBuf.Lang); /** * @alias ProtoBuf.DotProto.Parser * @expose */ ProtoBuf.DotProto.Parser = (function(ProtoBuf, Lang, Tokenizer) { "use strict"; /** * Constructs a new Parser. * @exports ProtoBuf.DotProto.Parser * @class A ProtoBuf .proto parser. * @param {string} proto Protocol source * @constructor */ var Parser = function(proto) { /** * Tokenizer. * @type {ProtoBuf.DotProto.Tokenizer} * @expose */ this.tn = new Tokenizer(proto); }; /** * Runs the parser. * @return {{package: string|null, messages: Array., enums: Array., imports: Array., options: object}} * @throws {Error} If the source cannot be parsed * @expose */ Parser.prototype.parse = function() { var topLevel = { "name": "[ROOT]", // temporary "package": null, "messages": [], "enums": [], "imports": [], "options": {}, "services": [] }; var token, header = true; do { token = this.tn.next(); if (token == null) { break; // No more messages } if (token == 'package') { if (!header) { throw(new Error("Illegal package definition at line "+this.tn.line+": Must be declared before the first message or enum")); } if (topLevel["package"] !== null) { throw(new Error("Illegal package definition at line "+this.tn.line+": Package already declared")); } topLevel["package"] = this._parsePackage(token); } else if (token == 'import') { if (!header) { throw(new Error("Illegal import definition at line "+this.tn.line+": Must be declared before the first message or enum")); } topLevel.imports.push(this._parseImport(token)); } else if (token === 'message') { this._parseMessage(topLevel, token); header = false; } else if (token === 'enum') { this._parseEnum(topLevel, token); header = false; } else if (token === 'option') { if (!header) { throw(new Error("Illegal option definition at line "+this.tn.line+": Must be declared before the first message or enum")); } this._parseOption(topLevel, token); } else if (token === 'service') { this._parseService(topLevel, token); } else if (token === 'extend') { this._parseExtend(topLevel, token); } else if (token === 'syntax') { this._parseIgnoredStatement(topLevel, token); } else { throw(new Error("Illegal top level declaration at line "+this.tn.line+": "+token)); } } while (true); delete topLevel["name"]; return topLevel; }; /** * Parses a number value. * @param {string} val Number value to parse * @return {number} Number * @throws {Error} If the number value is invalid * @private */ Parser.prototype._parseNumber = function(val) { var sign = 1; if (val.charAt(0) == '-') { sign = -1; val = val.substring(1); } if (Lang.NUMBER_DEC.test(val)) { return sign*parseInt(val, 10); } else if (Lang.NUMBER_HEX.test(val)) { return sign*parseInt(val.substring(2), 16); } else if (Lang.NUMBER_OCT.test(val)) { return sign*parseInt(val.substring(1), 8); } else if (Lang.NUMBER_FLT.test(val)) { return sign*parseFloat(val); } throw(new Error("Illegal number value at line "+this.tn.line+": "+(sign < 0 ? '-' : '')+val)); }; /** * Parses an ID value. * @param {string} val ID value to parse * @param {boolean=} neg Whether the ID may be negative, defaults to `false` * @returns {number} ID * @throws {Error} If the ID value is invalid * @private */ Parser.prototype._parseId = function(val, neg) { var id = -1; var sign = 1; if (val.charAt(0) == '-') { sign = -1; val = val.substring(1); } if (Lang.NUMBER_DEC.test(val)) { id = parseInt(val); } else if (Lang.NUMBER_HEX.test(val)) { id = parseInt(val.substring(2), 16); } else if (Lang.NUMBER_OCT.test(val)) { id = parseInt(val.substring(1), 8); } else { throw(new Error("Illegal ID value at line "+this.tn.line+": "+(sign < 0 ? '-' : '')+val)); } id = (sign*id)|0; // Force to 32bit if (!neg && id < 0) { throw(new Error("Illegal ID range at line "+this.tn.line+": "+(sign < 0 ? '-' : '')+val)); } return id; }; /** * Parses the package definition. * @param {string} token Initial token * @return {string} Package name * @throws {Error} If the package definition cannot be parsed * @private */ Parser.prototype._parsePackage = function(token) { token = this.tn.next(); if (!Lang.TYPEREF.test(token)) { throw(new Error("Illegal package name at line "+this.tn.line+": "+token)); } var pkg = token; token = this.tn.next(); if (token != Lang.END) { throw(new Error("Illegal end of package definition at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } return pkg; }; /** * Parses an import definition. * @param {string} token Initial token * @return {string} Import file name * @throws {Error} If the import definition cannot be parsed * @private */ Parser.prototype._parseImport = function(token) { token = this.tn.next(); if (token === "public") { token = this.tn.next(); } if (token !== Lang.STRINGOPEN) { throw(new Error("Illegal begin of import value at line "+this.tn.line+": "+token+" ('"+Lang.STRINGOPEN+"' expected)")); } var imported = this.tn.next(); token = this.tn.next(); if (token !== Lang.STRINGCLOSE) { throw(new Error("Illegal end of import value at line "+this.tn.line+": "+token+" ('"+Lang.STRINGCLOSE+"' expected)")); } token = this.tn.next(); if (token !== Lang.END) { throw(new Error("Illegal end of import definition at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } return imported; }; /** * Parses a namespace option. * @param {Object} parent Parent definition * @param {string} token Initial token * @throws {Error} If the option cannot be parsed * @private */ Parser.prototype._parseOption = function(parent, token) { token = this.tn.next(); var custom = false; if (token == Lang.COPTOPEN) { custom = true; token = this.tn.next(); } if (!Lang.NAME.test(token)) { // we can allow options of the form google.protobuf.* since they will just get ignored anyways if (!/google\.protobuf\./.test(token)) { throw(new Error("Illegal option name in message "+parent.name+" at line "+this.tn.line+": "+token)); } } var name = token; token = this.tn.next(); if (custom) { // (my_method_option).foo, (my_method_option), some_method_option if (token !== Lang.COPTCLOSE) { throw(new Error("Illegal custom option name delimiter in message "+parent.name+", option "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.COPTCLOSE+"' expected)")); } name = '('+name+')'; token = this.tn.next(); if (Lang.FQTYPEREF.test(token)) { name += token; token = this.tn.next(); } } if (token !== Lang.EQUAL) { throw(new Error("Illegal option operator in message "+parent.name+", option "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.EQUAL+"' expected)")); } var value; token = this.tn.next(); if (token === Lang.STRINGOPEN) { value = this.tn.next(); token = this.tn.next(); if (token !== Lang.STRINGCLOSE) { throw(new Error("Illegal end of option value in message "+parent.name+", option "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.STRINGCLOSE+"' expected)")); } } else { if (Lang.NUMBER.test(token)) { value = this._parseNumber(token, true); } else if (Lang.TYPEREF.test(token)) { value = token; } else { throw(new Error("Illegal option value in message "+parent.name+", option "+name+" at line "+this.tn.line+": "+token)); } } token = this.tn.next(); if (token !== Lang.END) { throw(new Error("Illegal end of option in message "+parent.name+", option "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } parent["options"][name] = value; }; /** * Parses an ignored block of the form ['keyword', 'typeref', '{' ... '}']. * @param {Object} parent Parent definition * @param {string} keyword Initial token * @throws {Error} If the directive cannot be parsed * @private */ Parser.prototype._parseIgnoredBlock = function(parent, keyword) { var token = this.tn.next(); if (!Lang.TYPEREF.test(token)) { throw(new Error("Illegal "+keyword+" type in "+parent.name+": "+token)); } var name = token; token = this.tn.next(); if (token !== Lang.OPEN) { throw(new Error("Illegal OPEN in "+parent.name+" after "+keyword+" "+name+" at line "+this.tn.line+": "+token)); } var depth = 1; do { token = this.tn.next(); if (token === null) { throw(new Error("Unexpected EOF in "+parent.name+", "+keyword+" (ignored) at line "+this.tn.line+": "+name)); } if (token === Lang.OPEN) { depth++; } else if (token === Lang.CLOSE) { token = this.tn.peek(); if (token === Lang.END) this.tn.next(); depth--; if (depth === 0) { break; } } } while(true); }; /** * Parses an ignored statement of the form ['keyword', ..., ';']. * @param {Object} parent Parent definition * @param {string} keyword Initial token * @throws {Error} If the directive cannot be parsed * @private */ Parser.prototype._parseIgnoredStatement = function(parent, keyword) { var token; do { token = this.tn.next(); if (token === null) { throw(new Error("Unexpected EOF in "+parent.name+", "+keyword+" (ignored) at line "+this.tn.line)); } if (token === Lang.END) break; } while (true); }; /** * Parses a service definition. * @param {Object} parent Parent definition * @param {string} keyword Initial token * @throws {Error} If the service cannot be parsed * @private */ Parser.prototype._parseService = function(parent, keyword) { var token = this.tn.next(); if (!Lang.NAME.test(token)) { throw(new Error("Illegal service name at line "+this.tn.line+": "+token)); } var name = token; var svc = { "name": name, "rpc": {}, "options": {} }; token = this.tn.next(); if (token !== Lang.OPEN) { throw(new Error("Illegal OPEN after service "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.OPEN+"' expected)")); } do { token = this.tn.next(); if (token === "option") { this._parseOption(svc, token); } else if (token === 'rpc') { this._parseServiceRPC(svc, token); } else if (token !== Lang.CLOSE) { throw(new Error("Illegal type for service "+name+" at line "+this.tn.line+": "+token)); } } while (token !== Lang.CLOSE); parent["services"].push(svc); }; /** * Parses a RPC service definition of the form ['rpc', name, (request), 'returns', (response)]. * @param {Object} svc Parent definition * @param {string} token Initial token * @private */ Parser.prototype._parseServiceRPC = function(svc, token) { var type = token; token = this.tn.next(); if (!Lang.NAME.test(token)) { throw(new Error("Illegal RPC method name in service "+svc["name"]+" at line "+this.tn.line+": "+token)); } var name = token; var method = { "request": null, "response": null, "options": {} }; token = this.tn.next(); if (token !== Lang.COPTOPEN) { throw(new Error("Illegal start of request type in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('"+Lang.COPTOPEN+"' expected)")); } token = this.tn.next(); if (!Lang.TYPEREF.test(token)) { throw(new Error("Illegal request type in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token)); } method["request"] = token; token = this.tn.next(); if (token != Lang.COPTCLOSE) { throw(new Error("Illegal end of request type in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('"+Lang.COPTCLOSE+"' expected)")) } token = this.tn.next(); if (token.toLowerCase() !== "returns") { throw(new Error("Illegal request/response delimiter in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('returns' expected)")); } token = this.tn.next(); if (token != Lang.COPTOPEN) { throw(new Error("Illegal start of response type in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('"+Lang.COPTOPEN+"' expected)")); } token = this.tn.next(); method["response"] = token; token = this.tn.next(); if (token !== Lang.COPTCLOSE) { throw(new Error("Illegal end of response type in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('"+Lang.COPTCLOSE+"' expected)")) } token = this.tn.next(); if (token === Lang.OPEN) { do { token = this.tn.next(); if (token === 'option') { this._parseOption(method, token); // <- will fail for the custom-options example } else if (token !== Lang.CLOSE) { throw(new Error("Illegal start of option in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('option' expected)")); } } while (token !== Lang.CLOSE); } else if (token !== Lang.END) { throw(new Error("Illegal method delimiter in RPC service "+svc["name"]+"#"+name+" at line "+this.tn.line+": "+token+" ('"+Lang.END+"' or '"+Lang.OPEN+"' expected)")); } if (typeof svc[type] === 'undefined') svc[type] = {}; svc[type][name] = method; }; /** * Parses a message definition. * @param {Object} parent Parent definition * @param {string} token First token * @return {Object} * @throws {Error} If the message cannot be parsed * @private */ Parser.prototype._parseMessage = function(parent, token) { /** @dict */ var msg = {}; // Note: At some point we might want to exclude the parser, so we need a dict. token = this.tn.next(); if (!Lang.NAME.test(token)) { throw(new Error("Illegal message name"+(parent ? " in message "+parent["name"] : "")+" at line "+this.tn.line+": "+token)); } msg["name"] = token; token = this.tn.next(); if (token != Lang.OPEN) { throw(new Error("Illegal OPEN after message "+msg.name+" at line "+this.tn.line+": "+token+" ('"+Lang.OPEN+"' expected)")); } msg["fields"] = []; // Note: Using arrays to support also browser that cannot preserve order of object keys. msg["enums"] = []; msg["messages"] = []; msg["options"] = {}; // msg["extensions"] = undefined do { token = this.tn.next(); if (token === Lang.CLOSE) { token = this.tn.peek(); if (token === Lang.END) this.tn.next(); break; } else if (Lang.RULE.test(token)) { this._parseMessageField(msg, token); } else if (token === "enum") { this._parseEnum(msg, token); } else if (token === "message") { this._parseMessage(msg, token); } else if (token === "option") { this._parseOption(msg, token); } else if (token === "extensions") { msg["extensions"] = this._parseExtensions(msg, token); } else if (token === "extend") { this._parseExtend(msg, token); } else { throw(new Error("Illegal token in message "+msg.name+" at line "+this.tn.line+": "+token+" (type or '"+Lang.CLOSE+"' expected)")); } } while (true); parent["messages"].push(msg); return msg; }; /** * Parses a message field. * @param {Object} msg Message definition * @param {string} token Initial token * @throws {Error} If the message field cannot be parsed * @private */ Parser.prototype._parseMessageField = function(msg, token) { /** @dict */ var fld = {}; fld["rule"] = token; token = this.tn.next(); if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token)) { throw(new Error("Illegal field type in message "+msg.name+" at line "+this.tn.line+": "+token)); } fld["type"] = token; token = this.tn.next(); if (!Lang.NAME.test(token)) { throw(new Error("Illegal field name in message "+msg.name+" at line "+this.tn.line+": "+token)); } fld["name"] = token; token = this.tn.next(); if (token !== Lang.EQUAL) { throw(new Error("Illegal field number operator in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token+" ('"+Lang.EQUAL+"' expected)")); } token = this.tn.next(); try { fld["id"] = this._parseId(token); } catch (e) { throw(new Error("Illegal field id in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token)); } /** @dict */ fld["options"] = {}; token = this.tn.next(); if (token === Lang.OPTOPEN) { this._parseFieldOptions(msg, fld, token); token = this.tn.next(); } if (token !== Lang.END) { throw(new Error("Illegal field delimiter in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } msg["fields"].push(fld); }; /** * Parses a set of field option definitions. * @param {Object} msg Message definition * @param {Object} fld Field definition * @param {string} token Initial token * @throws {Error} If the message field options cannot be parsed * @private */ Parser.prototype._parseFieldOptions = function(msg, fld, token) { var first = true; do { token = this.tn.next(); if (token === Lang.OPTCLOSE) { break; } else if (token === Lang.OPTEND) { if (first) { throw(new Error("Illegal start of message field options in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token)); } token = this.tn.next(); } this._parseFieldOption(msg, fld, token); first = false; } while (true); }; /** * Parses a single field option. * @param {Object} msg Message definition * @param {Object} fld Field definition * @param {string} token Initial token * @throws {Error} If the mesage field option cannot be parsed * @private */ Parser.prototype._parseFieldOption = function(msg, fld, token) { var custom = false; if (token === Lang.COPTOPEN) { token = this.tn.next(); custom = true; } if (!Lang.NAME.test(token)) { throw(new Error("Illegal field option in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token)); } var name = token; token = this.tn.next(); if (custom) { if (token !== Lang.COPTCLOSE) { throw(new Error("Illegal custom field option name delimiter in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token+" (')' expected)")); } name = '('+name+')'; token = this.tn.next(); if (Lang.FQTYPEREF.test(token)) { name += token; token = this.tn.next(); } } if (token !== Lang.EQUAL) { throw(new Error("Illegal field option operation in message "+msg.name+"#"+fld.name+" at line "+this.tn.line+": "+token+" ('=' expected)")); } var value; token = this.tn.next(); if (token === Lang.STRINGOPEN) { value = this.tn.next(); token = this.tn.next(); if (token != Lang.STRINGCLOSE) { throw(new Error("Illegal end of field value in message "+msg.name+"#"+fld.name+", option "+name+" at line "+this.tn.line+": "+token+" ('"+Lang.STRINGCLOSE+"' expected)")); } } else if (Lang.NUMBER.test(token, true)) { value = this._parseNumber(token, true); } else if (Lang.BOOL.test(token)) { value = token.toLowerCase() === 'true'; } else if (Lang.TYPEREF.test(token)) { value = token; // TODO: Resolve? } else { throw(new Error("Illegal field option value in message "+msg.name+"#"+fld.name+", option "+name+" at line "+this.tn.line+": "+token)); } fld["options"][name] = value; }; /** * Parses an enum. * @param {Object} msg Message definition * @param {string} token Initial token * @throws {Error} If the enum cannot be parsed * @private */ Parser.prototype._parseEnum = function(msg, token) { /** @dict */ var enm = {}; token = this.tn.next(); if (!Lang.NAME.test(token)) { throw(new Error("Illegal enum name in message "+msg.name+" at line "+this.tn.line+": "+token)); } enm["name"] = token; token = this.tn.next(); if (token !== Lang.OPEN) { throw(new Error("Illegal OPEN after enum "+enm.name+" at line "+this.tn.line+": "+token)); } enm["values"] = []; enm["options"] = {}; do { token = this.tn.next(); if (token === Lang.CLOSE) { token = this.tn.peek(); if (token === Lang.END) this.tn.next(); break; } if (token == 'option') { this._parseOption(enm, token); } else { if (!Lang.NAME.test(token)) { throw(new Error("Illegal enum value name in enum "+enm.name+" at line "+this.tn.line+": "+token)); } this._parseEnumValue(enm, token); } } while (true); msg["enums"].push(enm); }; /** * Parses an enum value. * @param {Object} enm Enum definition * @param {string} token Initial token * @throws {Error} If the enum value cannot be parsed * @private */ Parser.prototype._parseEnumValue = function(enm, token) { /** @dict */ var val = {}; val["name"] = token; token = this.tn.next(); if (token !== Lang.EQUAL) { throw(new Error("Illegal enum value operator in enum "+enm.name+" at line "+this.tn.line+": "+token+" ('"+Lang.EQUAL+"' expected)")); } token = this.tn.next(); try { val["id"] = this._parseId(token, true); } catch (e) { throw(new Error("Illegal enum value id in enum "+enm.name+" at line "+this.tn.line+": "+token)); } enm["values"].push(val); token = this.tn.next(); if (token === Lang.OPTOPEN) { var opt = { 'options' : {} }; // TODO: Actually expose them somehow. this._parseFieldOptions(enm, opt, token); token = this.tn.next(); } if (token !== Lang.END) { throw(new Error("Illegal enum value delimiter in enum "+enm.name+" at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } }; /** * Parses an extensions statement. * @param {Object} msg Message object * @param {string} token Initial token * @throws {Error} If the extensions statement cannot be parsed * @private */ Parser.prototype._parseExtensions = function(msg, token) { /** @type {Array.} */ var range = []; token = this.tn.next(); if (token === "min") { // FIXME: Does the official implementation support this? range.push(Lang.ID_MIN); } else if (token === "max") { range.push(Lang.ID_MAX); } else { range.push(this._parseNumber(token)); } token = this.tn.next(); if (token !== 'to') { throw("Illegal extensions delimiter in message "+msg.name+" at line "+this.tn.line+" ('to' expected)"); } token = this.tn.next(); if (token === "min") { range.push(Lang.ID_MIN); } else if (token === "max") { range.push(Lang.ID_MAX); } else { range.push(this._parseNumber(token)); } token = this.tn.next(); if (token !== Lang.END) { throw(new Error("Illegal extension delimiter in message "+msg.name+" at line "+this.tn.line+": "+token+" ('"+Lang.END+"' expected)")); } return range; }; /** * Parses an extend block. * @param {Object} parent Parent object * @param {string} token Initial token * @throws {Error} If the extend block cannot be parsed * @private */ Parser.prototype._parseExtend = function(parent, token) { token = this.tn.next(); if (!Lang.TYPEREF.test(token)) { throw(new Error("Illegal extended message name at line "+this.tn.line+": "+token)); } /** @dict */ var ext = {}; ext["ref"] = token; ext["fields"] = []; token = this.tn.next(); if (token !== Lang.OPEN) { throw(new Error("Illegal OPEN in extend "+ext.name+" at line "+this.tn.line+": "+token+" ('"+Lang.OPEN+"' expected)")); } do { token = this.tn.next(); if (token === Lang.CLOSE) { token = this.tn.peek(); if (token == Lang.END) this.tn.next(); break; } else if (Lang.RULE.test(token)) { this._parseMessageField(ext, token); } else { throw(new Error("Illegal token in extend "+ext.name+" at line "+this.tn.line+": "+token+" (rule or '"+Lang.CLOSE+"' expected)")); } } while (true); parent["messages"].push(ext); return ext; }; /** * Returns a string representation of this object. * @returns {string} String representation as of "Parser" */ Parser.prototype.toString = function() { return "Parser"; }; return Parser; })(ProtoBuf, ProtoBuf.Lang, ProtoBuf.DotProto.Tokenizer); /** * @alias ProtoBuf.Reflect * @expose */ ProtoBuf.Reflect = (function(ProtoBuf) { "use strict"; /** * @exports ProtoBuf.Reflect * @namespace */ var Reflect = {}; /** * Constructs a Reflect base class. * @exports ProtoBuf.Reflect.T * @constructor * @param {ProtoBuf.Reflect.T} parent Parent object * @param {string} name Object name */ var T = function(parent, name) { /** * Parent object. * @type {ProtoBuf.Reflect.T|null} * @expose */ this.parent = parent; /** * Object name in namespace. * @type {string} * @expose */ this.name = name; }; /** * Returns the fully qualified name of this object. * @returns {string} Fully qualified name as of ".PATH.TO.THIS" * @expose */ T.prototype.fqn = function() { var name = this.name, ptr = this; do { ptr = ptr.parent; if (ptr == null) break; name = ptr.name+"."+name; } while (true); return name; }; /** * Returns a string representation of this Reflect object (its fully qualified name). * @param {boolean=} includeClass Set to true to include the class name. Defaults to false. * @return String representation * @expose */ T.prototype.toString = function(includeClass) { var name = this.fqn(); if (includeClass) { if (this instanceof Message) { name = "Message "+name; } else if (this instanceof Message.Field) { name = "Message.Field "+name; } else if (this instanceof Enum) { name = "Enum "+name; } else if (this instanceof Enum.Value) { name = "Enum.Value "+name; } else if (this instanceof Service) { name = "Service "+name; } else if (this instanceof Service.Method) { if (this instanceof Service.RPCMethod) { name = "Service.RPCMethod "+name; } else { name = "Service.Method "+name; // Should not happen as it is abstract } } else if (this instanceof Namespace) { name = "Namespace "+name; } } return name; }; /** * Builds this type. * @throws {Error} If this type cannot be built directly * @expose */ T.prototype.build = function() { throw(new Error(this.toString(true)+" cannot be built directly")); }; /** * @alias ProtoBuf.Reflect.T * @expose */ Reflect.T = T; /** * Constructs a new Namespace. * @exports ProtoBuf.Reflect.Namespace * @param {ProtoBuf.Reflect.Namespace|null} parent Namespace parent * @param {string} name Namespace name * @param {Object.} options Namespace options * @constructor * @extends ProtoBuf.Reflect.T */ var Namespace = function(parent, name, options) { T.call(this, parent, name); /** * Children inside the namespace. * @type {Array.} */ this.children = []; /** * Options. * @type {Object.} */ this.options = options || {}; }; // Extends T Namespace.prototype = Object.create(T.prototype); /** * Returns an array of the namespace's children. * @param {ProtoBuf.Reflect.T=} type Filter type (returns instances of this type only). Defaults to null (all children). * @return {Array.} * @expose */ Namespace.prototype.getChildren = function(type) { type = type || null; if (type == null) { return this.children.slice(); } var children = []; for (var i=0; i} Runtime namespace * @expose */ Namespace.prototype.build = function() { /** @dict */ var ns = {}; var children = this.getChildren(), child; for (var i=0; i} */ Namespace.prototype.buildOpt = function() { var opt = {}; var keys = Object.keys(this.options); for (var i=0; i}null} Option value or NULL if there is no such option */ Namespace.prototype.getOption = function(name) { if (typeof name == 'undefined') { return this.options; } return typeof this.options[name] != 'undefined' ? this.options[name] : null; }; /** * @alias ProtoBuf.Reflect.Namespace * @expose */ Reflect.Namespace = Namespace; /** * Constructs a new Message. * @exports ProtoBuf.Reflect.Message * @param {ProtoBuf.Reflect.Namespace} parent Parent message or namespace * @param {string} name Message name * @param {Object.} options Message options * @constructor * @extends ProtoBuf.Reflect.Namespace */ var Message = function(parent, name, options) { Namespace.call(this, parent, name, options); /** * Extensions range. * @type {!Array.} * @expose */ this.extensions = [ProtoBuf.Lang.ID_MIN, ProtoBuf.Lang.ID_MAX]; /** * Runtime message class. * @type {?function(new:ProtoBuf.Builder.Message)} * @expose */ this.clazz = null; }; // Extends Namespace Message.prototype = Object.create(Namespace.prototype); /** * Builds the message and returns the runtime counterpart, which is a fully functional class. * @see ProtoBuf.Builder.Message * @param {boolean=} rebuild Whether to rebuild or not, defaults to false * @return {ProtoBuf.Reflect.Message} Message class * @throws {Error} If the message cannot be built * @expose */ Message.prototype.build = function(rebuild) { if (this.clazz && !rebuild) return this.clazz; // We need to create a prototyped Message class in an isolated scope var clazz = (function(ProtoBuf, T) { var fields = T.getChildren(Reflect.Message.Field); /** * Constructs a new runtime Message. * @name ProtoBuf.Builder.Message * @class Barebone of all runtime messages. * @param {Object.|...[string]} values Preset values * @constructor * @throws {Error} If the message cannot be created */ var Message = function(values) { ProtoBuf.Builder.Message.call(this); var i, field; // Create fields on the object itself to allow setting and getting through Message#fieldname for (i=0; i} * @expose */ var O_o; // for cc if (Object.defineProperty) { Object.defineProperty(Message, '$options', { 'value': T.buildOpt(), 'enumerable': false, 'configurable': false, 'writable': false }); } return Message; })(ProtoBuf, this); // Static enums and prototyped sub-messages var children = this.getChildren(); for (var i=0; i 0)) { var tag = buffer.readVarint32(); var wireType = tag & 0x07, id = tag >> 3; var field = this.getChild(id); // Message.Field only if (!field) { // "messages created by your new code can be parsed by your old code: old binaries simply ignore the new field when parsing." switch (wireType) { case ProtoBuf.WIRE_TYPES.VARINT: buffer.readVarint32(); break; case ProtoBuf.WIRE_TYPES.BITS32: buffer.offset += 4; break; case ProtoBuf.WIRE_TYPES.BITS64: buffer.offset += 8; break; case ProtoBuf.WIRE_TYPES.LDELIM: var len = buffer.readVarint32(); buffer.offset += len; break; default: throw(new Error("Illegal wire type of unknown field "+id+" in "+this.toString(true)+"#decode: "+wireType)); } continue; } if (field.repeated && !field.options["packed"]) { msg.add(field.name, field.decode(wireType, buffer)); } else { msg.set(field.name, field.decode(wireType, buffer)); } } // Check if all required fields are present var fields = this.getChildren(ProtoBuf.Reflect.Field); for (var i=0; i=} options Options * @constructor * @extends ProtoBuf.Reflect.T */ var Field = function(message, rule, type, name, id, options) { T.call(this, message, name); /** * Message field required flag. * @type {boolean} * @expose */ this.required = rule == "required"; /** * Message field repeated flag. * @type {boolean} * @expose */ this.repeated = rule == "repeated"; /** * Message field type. Type reference string if unresolved, protobuf type if resolved. * @type {string|{name: string, wireType: number} * @expose */ this.type = type; /** * Resolved type reference inside the global namespace. * @type {ProtoBuf.Reflect.T|null} * @expose */ this.resolvedType = null; /** * Unique message field id. * @type {number} * @expose */ this.id = id; /** * Message field options. * @type {!Object.} * @dict * @expose */ this.options = options || {}; /** * Original field name. * @type {string} * @expose */ this.originalName = this.name; // Used to revert camelcase transformation on naming collisions // Convert field names to camel case notation if the override is set if (ProtoBuf.convertFieldsToCamelCase) { this.name = this.name.replace(/_([a-zA-Z])/g, function($0, $1) { return $1.toUpperCase(); }); } }; // Extends T Field.prototype = Object.create(T.prototype); /** * Checks if the given value can be set for this field. * @param {*} value Value to check * @param {boolean=} skipRepeated Whether to skip the repeated value check or not. Defaults to false. * @return {*} Verified, maybe adjusted, value * @throws {Error} If the value cannot be set for this field * @expose */ Field.prototype.verifyValue = function(value, skipRepeated) { skipRepeated = skipRepeated || false; if (value === null) { // NULL values for optional fields if (this.required) { throw(new Error("Illegal value for "+this.toString(true)+": "+value+" (required)")); } return null; } var i; if (this.repeated && !skipRepeated) { // Repeated values as arrays if (!ProtoBuf.Util.isArray(value)) { value = [value]; } var res = []; for (i=0; i>> 0; // Do not cast NaN as it'd become 0 } if (ProtoBuf.Long) { // Signed 64bit if (this.type == ProtoBuf.TYPES["int64"] || this.type == ProtoBuf.TYPES["sint64"] || this.type == ProtoBuf.TYPES["sfixed64"]) { if (!(typeof value == 'object' && value instanceof ProtoBuf.Long)) { return ProtoBuf.Long.fromNumber(value, false); } return value.unsigned ? value.toSigned() : value; } // Unsigned 64bit if (this.type == ProtoBuf.TYPES["uint64"] || this.type == ProtoBuf.TYPES["fixed64"]) { if (!(typeof value == 'object' && value instanceof ProtoBuf.Long)) { return ProtoBuf.Long.fromNumber(value, true); } return value.unsigned ? value : value.toUnsigned(); } } // Bool if (this.type == ProtoBuf.TYPES["bool"]) { if (typeof value === 'string') return value === 'true'; else return !!value; } // Float if (this.type == ProtoBuf.TYPES["float"] || this.type == ProtoBuf.TYPES["double"]) { return parseFloat(value); // May also become NaN, +Infinity, -Infinity } // Length-delimited string if (this.type == ProtoBuf.TYPES["string"]) { return ""+value; } // Length-delimited bytes if (this.type == ProtoBuf.TYPES["bytes"]) { if (value && value instanceof ByteBuffer) { return value; } return ByteBuffer.wrap(value); } // Constant enum value if (this.type == ProtoBuf.TYPES["enum"]) { var values = this.resolvedType.getChildren(Enum.Value); for (i=0; i 1) { // We need to move the contents var contents = buffer.slice(start, buffer.offset); start += varintLen-1; buffer.offset = start; buffer.append(contents); } buffer.writeVarint32(len, start-varintLen); } else { // "If your message definition has repeated elements (without the [packed=true] option), the encoded // message has zero or more key-value pairs with the same tag number" for (i=0; i value.length) { // Forgot to flip? buffer = buffer.clone().flip(); } buffer.writeVarint32(value.remaining()); buffer.append(value); // Embedded message } else if (this.type == ProtoBuf.TYPES["message"]) { var bb = new ByteBuffer().LE(); this.resolvedType.encode(value, bb); buffer.writeVarint32(bb.offset); buffer.append(bb.flip()); } else { // We should never end here throw(new Error("[INTERNAL] Illegal value to encode in "+this.toString(true)+": "+value+" (unknown type)")); } return buffer; }; /** * Decode the field value from the specified buffer. * @param {number} wireType Leading wire type * @param {ByteBuffer} buffer ByteBuffer to decode from * @param {boolean=} skipRepeated Whether to skip the repeated check or not. Defaults to false. * @return {*} Decoded value * @throws {Error} If the field cannot be decoded * @expose */ Field.prototype.decode = function(wireType, buffer, skipRepeated) { var value, nBytes; if (wireType != this.type.wireType && (skipRepeated || (wireType != ProtoBuf.WIRE_TYPES.LDELIM || !this.repeated))) { throw(new Error("Illegal wire type for field "+this.toString(true)+": "+wireType+" ("+this.type.wireType+" expected)")); } if (wireType == ProtoBuf.WIRE_TYPES.LDELIM && this.repeated && this.options["packed"]) { if (!skipRepeated) { nBytes = buffer.readVarint32(); nBytes = buffer.offset + nBytes; // Limit var values = []; while (buffer.offset < nBytes) { values.push(this.decode(this.type.wireType, buffer, true)); } return values; } // Read the next value otherwise... } // 32bit signed varint if (this.type == ProtoBuf.TYPES["int32"]) { return buffer.readVarint32() | 0; } // 32bit unsigned varint if (this.type == ProtoBuf.TYPES["uint32"]) { return buffer.readVarint32() >>> 0; } // 32bit signed varint zig-zag if (this.type == ProtoBuf.TYPES["sint32"]) { return buffer.readZigZagVarint32() | 0; } // Fixed 32bit unsigned if (this.type == ProtoBuf.TYPES["fixed32"]) { return buffer.readUint32() >>> 0; } // Fixed 32bit signed if (this.type == ProtoBuf.TYPES["sfixed32"]) { return buffer.readInt32() | 0; } // 64bit signed varint if (this.type == ProtoBuf.TYPES["int64"]) { return buffer.readVarint64(); } // 64bit unsigned varint if (this.type == ProtoBuf.TYPES["uint64"]) { return buffer.readVarint64().toUnsigned(); } // 64bit signed varint zig-zag if (this.type == ProtoBuf.TYPES["sint64"]) { return buffer.readZigZagVarint64(); } // Fixed 64bit unsigned if (this.type == ProtoBuf.TYPES["fixed64"]) { return buffer.readUint64(); } // Fixed 64bit signed if (this.type == ProtoBuf.TYPES["sfixed64"]) { return buffer.readInt64(); } // Bool varint if (this.type == ProtoBuf.TYPES["bool"]) { return !!buffer.readVarint32(); } // Constant enum value varint) if (this.type == ProtoBuf.TYPES["enum"]) { return buffer.readVarint32(); // The following Builder.Message#set will already throw } // 32bit float if (this.type == ProtoBuf.TYPES["float"]) { return buffer.readFloat(); } // 64bit float if (this.type == ProtoBuf.TYPES["double"]) { return buffer.readDouble(); } // Length-delimited string if (this.type == ProtoBuf.TYPES["string"]){ return buffer.readVString(); } // Length-delimited bytes if (this.type == ProtoBuf.TYPES["bytes"]) { nBytes = buffer.readVarint32(); if (buffer.remaining() < nBytes) { throw(new Error("Illegal number of bytes for "+this.toString(true)+": "+nBytes+" required but got only "+buffer.remaining())); } value = buffer.clone(); // Offset already set value.length = value.offset+nBytes; buffer.offset += nBytes; return value; } // Length-delimited embedded message if (this.type == ProtoBuf.TYPES["message"]) { nBytes = buffer.readVarint32(); return this.resolvedType.decode(buffer, nBytes); } // We should never end here throw(new Error("[INTERNAL] Illegal wire type for "+this.toString(true)+": "+wireType)); }; /** * @alias ProtoBuf.Reflect.Message.Field * @expose */ Reflect.Message.Field = Field; /** * Constructs a new Enum. * @exports ProtoBuf.Reflect.Enum * @param {!ProtoBuf.Reflect.T} parent Parent Reflect object * @param {string} name Enum name * @param {Object.=} options Enum options * @constructor * @extends ProtoBuf.Reflect.Namespace */ var Enum = function(parent, name, options) { Namespace.call(this, parent, name, options); /** * Runtime enum object. * @type {Object.|null} * @expose */ this.object = null; }; // Extends Namespace Enum.prototype = Object.create(Namespace.prototype); /** * Builds this enum and returns the runtime counterpart. * @return {Object} * @expose */ Enum.prototype.build = function() { var enm = {}; var values = this.getChildren(Enum.Value); for (var i=0; i=} options Options * @constructor * @extends ProtoBuf.Reflect.Namespace */ var Service = function(root, name, options) { Namespace.call(this, root, name, options); /** * Built runtime service class. * @type {?function(new:ProtoBuf.Builder.Service)} */ this.clazz = null; }; // Extends Namespace Service.prototype = Object.create(Namespace.prototype); /** * Builds the service and returns the runtime counterpart, which is a fully functional class. * @see ProtoBuf.Builder.Service * @param {boolean=} rebuild Whether to rebuild or not * @return {Function} Service class * @throws {Error} If the message cannot be built * @expose */ Service.prototype.build = function(rebuild) { if (this.clazz && !rebuild) return this.clazz; return this.clazz = (function(ProtoBuf, T) { /** * Constructs a new runtime Service. * @name ProtoBuf.Builder.Service * @param {function(string, ProtoBuf.Builder.Message, function(Error, ProtoBuf.Builder.Message=))=} rpcImpl RPC implementation receiving the method name and the message * @class Barebone of all runtime services. * @constructor * @throws {Error} If the service cannot be created */ var Service = function(rpcImpl) { ProtoBuf.Builder.Service.call(this); /** * Service implementation. * @name ProtoBuf.Builder.Service#rpcImpl * @type {!function(string, ProtoBuf.Builder.Message, function(Error, ProtoBuf.Builder.Message=))} * @expose */ this.rpcImpl = rpcImpl || function(name, msg, callback) { // This is what a user has to implement: A function receiving the method name, the actual message to // send (type checked) and the callback that's either provided with the error as its first // argument or null and the actual response message. setTimeout(callback.bind(this, new Error("Not implemented, see: https://github.com/dcodeIO/ProtoBuf.js/wiki/Services")), 0); // Must be async! }; }; // Extends ProtoBuf.Builder.Service Service.prototype = Object.create(ProtoBuf.Builder.Service.prototype); if (Object.defineProperty) { Object.defineProperty(Service, "$options", { "value": T.buildOpt(), "enumerable": false, "configurable": false, "writable": false }); Object.defineProperty(Service.prototype, "$options", { "value": Service["$options"], "enumerable": false, "configurable": false, "writable": false }); } /** * Asynchronously performs an RPC call using the given RPC implementation. * @name ProtoBuf.Builder.Service.[Method] * @function * @param {!function(string, ProtoBuf.Builder.Message, function(Error, ProtoBuf.Builder.Message=))} rpcImpl RPC implementation * @param {ProtoBuf.Builder.Message} req Request * @param {function(Error, (ProtoBuf.Builder.Message|ByteBuffer|Buffer|string)=)} callback Callback receiving * the error if any and the response either as a pre-parsed message or as its raw bytes * @abstract */ /** * Asynchronously performs an RPC call using the instance's RPC implementation. * @name ProtoBuf.Builder.Service#[Method] * @function * @param {ProtoBuf.Builder.Message} req Request * @param {function(Error, (ProtoBuf.Builder.Message|ByteBuffer|Buffer|string)=)} callback Callback receiving * the error if any and the response either as a pre-parsed message or as its raw bytes * @abstract */ var rpc = T.getChildren(Reflect.Service.RPCMethod); for (var i=0; i=} options Options * @constructor * @extends ProtoBuf.Reflect.T */ var Method = function(svc, name, options) { T.call(this, svc, name); /** * Options. * @type {Object.} * @expose */ this.options = options || {}; }; // Extends T Method.prototype = Object.create(T.prototype); /** * Builds the method's '$options' property. * @name ProtoBuf.Reflect.Service.Method#buildOpt * @function * @return {Object.} */ Method.prototype.buildOpt = Namespace.prototype.buildOpt; /** * @alias ProtoBuf.Reflect.Service.Method * @expose */ Reflect.Service.Method = Method; /** * RPC service method. * @exports ProtoBuf.Reflect.Service.RPCMethod * @param {!ProtoBuf.Reflect.Service} svc Service * @param {string} name Method name * @param {string} request Request message name * @param {string} response Response message name * @param {Object.=} options Options * @constructor * @extends ProtoBuf.Reflect.Service.Method */ var RPCMethod = function(svc, name, request, response, options) { Method.call(this, svc, name, options); /** * Request message name. * @type {string} * @expose */ this.requestName = request; /** * Response message name. * @type {string} * @expose */ this.responseName = response; /** * Resolved request message type. * @type {ProtoBuf.Reflect.Message} * @expose */ this.resolvedRequestType = null; /** * Resolved response message type. * @type {ProtoBuf.Reflect.Message} * @expose */ this.resolvedResponseType = null; }; // Extends Method RPCMethod.prototype = Object.create(Method.prototype); /** * @alias ProtoBuf.Reflect.Service.RPCMethod * @expose */ Reflect.Service.RPCMethod = RPCMethod; return Reflect; })(ProtoBuf); /** * @alias ProtoBuf.Builder * @expose */ ProtoBuf.Builder = (function(ProtoBuf, Lang, Reflect) { "use strict"; /** * Constructs a new Builder. * @exports ProtoBuf.Builder * @class Provides the functionality to build protocol messages. * @constructor */ var Builder = function() { /** * Namespace. * @type {ProtoBuf.Reflect.Namespace} * @expose */ this.ns = new Reflect.Namespace(null, ""); // Global namespace /** * Namespace pointer. * @type {ProtoBuf.Reflect.T} * @expose */ this.ptr = this.ns; /** * Resolved flag. * @type {boolean} * @expose */ this.resolved = false; /** * The current building result. * @type {Object.|null} * @expose */ this.result = null; /** * Imported files. * @type {Array.} * @expose */ this.files = {}; /** * Import root override. * @type {?string} * @expose */ this.importRoot = null; }; /** * Resets the pointer to the global namespace. * @expose */ Builder.prototype.reset = function() { this.ptr = this.ns; }; /** * Defines a package on top of the current pointer position and places the pointer on it. * @param {string} pkg * @param {Object.=} options * @return {ProtoBuf.Builder} this * @throws {Error} If the package name is invalid * @expose */ Builder.prototype.define = function(pkg, options) { if (typeof pkg !== 'string' || !Lang.TYPEREF.test(pkg)) { throw(new Error("Illegal package name: "+pkg)); } var part = pkg.split("."), i; for (i=0; i} def Definition * @return {boolean} true if valid, else false * @expose */ Builder.isValidMessage = function(def) { // Messages require a string name if (typeof def["name"] !== 'string' || !Lang.NAME.test(def["name"])) { return false; } // Messages must not contain values (that'd be an enum) or methods (that'd be a service) if (typeof def["values"] !== 'undefined' || typeof def["rpc"] !== 'undefined') { return false; } // Fields, enums and messages are arrays if provided var i; if (typeof def["fields"] !== 'undefined') { if (!ProtoBuf.Util.isArray(def["fields"])) { return false; } var ids = [], id; // IDs must be unique for (i=0; i= 0) { return false; } ids.push(id); } ids = null; } if (typeof def["enums"] !== 'undefined') { if (!ProtoBuf.Util.isArray(def["enums"])) { return false; } for (i=0; i var keys = Object.keys(def["options"]); for (var i=0; i>} defs Messages, enums or services to create * @return {ProtoBuf.Builder} this * @throws {Error} If a message definition is invalid * @expose */ Builder.prototype.create = function(defs) { if (!defs) return; // Nothing to create if (!ProtoBuf.Util.isArray(defs)) { defs = [defs]; } if (defs.length == 0) return; // It's quite hard to keep track of scopes and memory here, so let's do this iteratively. var stack = [], def, obj, subObj, i, j; stack.push(defs); // One level [a, b, c] while (stack.length > 0) { defs = stack.pop(); if (ProtoBuf.Util.isArray(defs)) { // Stack always contains entire namespaces while (defs.length > 0) { def = defs.shift(); // Namespace always contains an array of messages, enums and services if (Builder.isValidMessage(def)) { obj = new Reflect.Message(this.ptr, def["name"], def["options"]); // Create fields if (def["fields"] && def["fields"].length > 0) { for (i=0; i 0) { for (i=0; i 0) { for (i=0; i ProtoBuf.Lang.ID_MAX) { obj.extensions[1] = ProtoBuf.Lang.ID_MAX; } } this.ptr.addChild(obj); // Add to current namespace if (subObj.length > 0) { stack.push(defs); // Push the current level back defs = subObj; // Continue processing sub level subObj = null; this.ptr = obj; // And move the pointer to this namespace obj = null; continue; } subObj = null; obj = null; } else if (Builder.isValidEnum(def)) { obj = new Reflect.Enum(this.ptr, def["name"], def["options"]); for (i=0; i obj.extensions[1]) { throw(new Error("Illegal extended field id in message "+obj.name+": "+def['fields'][i]['id']+" ("+obj.extensions.join(' to ')+" expected)")); } obj.addChild(new Reflect.Message.Field(obj, def["fields"][i]["rule"], def["fields"][i]["type"], def["fields"][i]["name"], def["fields"][i]["id"], def["fields"][i]["options"])); } /* if (this.ptr instanceof Reflect.Message) { this.ptr.addChild(obj); // Reference the extended message here to enable proper lookups } */ } else { if (!/\.?google\.protobuf\./.test(def["ref"])) { // Silently skip internal extensions throw(new Error("Extended message "+def["ref"]+" is not defined")); } } } else { throw(new Error("Not a valid message, enum, service or extend definition: "+JSON.stringify(def))); } def = null; } // Break goes here } else { throw(new Error("Not a valid namespace definition: "+JSON.stringify(defs))); } defs = null; this.ptr = this.ptr.parent; // This namespace is s done } this.resolved = false; // Require re-resolve this.result = null; // Require re-build return this; }; /** * Tests if the specified file is a valid import. * @param {string} filename * @returns {boolean} true if valid, false if it should be skipped * @expose */ Builder.isValidImport = function(filename) { // Ignore google/protobuf/descriptor.proto (for example) as it makes use of low-level // bootstrapping directives that are not required and therefore cannot be parsed by ProtoBuf.js. return !(/google\/protobuf\//.test(filename)); }; /** * Imports another definition into this builder. * @param {Object.} json Parsed import * @param {(string|{root: string, file: string})=} filename Imported file name * @return {ProtoBuf.Builder} this * @throws {Error} If the definition or file cannot be imported * @expose */ Builder.prototype["import"] = function(json, filename) { if (typeof filename === 'string') { if (ProtoBuf.Util.IS_NODE) { var path = require("path"); filename = path.resolve(filename); } if (!!this.files[filename]) { this.reset(); return this; // Skip duplicate imports } this.files[filename] = true; } if (!!json['imports'] && json['imports'].length > 0) { var importRoot, delim = '/', resetRoot = false; if (typeof filename === 'object') { // If an import root is specified, override this.importRoot = filename["root"]; resetRoot = true; // ... and reset afterwards importRoot = this.importRoot; filename = filename["file"]; if (importRoot.indexOf("\\") >= 0 || filename.indexOf("\\") >= 0) delim = '\\'; } else if (typeof filename === 'string') { if (this.importRoot) { // If import root is overridden, use it importRoot = this.importRoot; } else { // Otherwise compute from filename if (filename.indexOf("/") >= 0) { // Unix importRoot = filename.replace(/\/[^\/]*$/, ""); if (/* /file.proto */ importRoot === "") importRoot = "/"; } else if (filename.indexOf("\\") >= 0) { // Windows importRoot = filename.replace(/\\[^\\]*$/, ""); delim = '\\'; } else { importRoot = "."; } } } else { importRoot = null; } for (var i=0; i= 0) { return false; } ids.push(id); } ids = null; } return true; }; /** * Resolves all namespace objects. * @throws {Error} If a type cannot be resolved * @expose */ Builder.prototype.resolveAll = function() { // Resolve all reflected objects var res; if (this.ptr == null || typeof this.ptr.type === 'object') return; // Done (already resolved) if (this.ptr instanceof Reflect.Namespace) { // Build all children var children = this.ptr.getChildren(); for (var i=0; i} * @throws {Error} If a type could not be resolved * @expose */ Builder.prototype.build = function(path) { this.reset(); if (!this.resolved) { this.resolveAll(); this.resolved = true; this.result = null; // Require re-build } if (this.result == null) { // (Re-)Build this.result = this.ns.build(); } if (!path) { return this.result; } else { var part = path.split("."); var ptr = this.result; // Build namespace pointer (no hasChild etc.) for (var i=0; i=} options Top level options * @return {ProtoBuf.Builder} New Builder * @expose */ ProtoBuf.newBuilder = function(pkg, options) { var builder = new ProtoBuf.Builder(); if (typeof pkg !== 'undefined' && pkg !== null) { builder.define(pkg, options); } return builder; }; /** * Loads a .json definition and returns the Builder. * @param {!*|string} json JSON definition * @param {(ProtoBuf.Builder|string|{root: string, file: string})=} builder Builder to append to. Will create a new one if omitted. * @param {(string|{root: string, file: string})=} filename The corresponding file name if known. Must be specified for imports. * @return {ProtoBuf.Builder} Builder to create new messages * @throws {Error} If the definition cannot be parsed or built * @expose */ ProtoBuf.loadJson = function(json, builder, filename) { if (typeof builder === 'string' || (builder && typeof builder["file"] === 'string' && typeof builder["root"] === 'string')) { filename = builder; builder = null; } if (!builder || typeof builder !== 'object') builder = ProtoBuf.newBuilder(); if (typeof json === 'string') json = JSON.parse(json); builder["import"](json, filename); builder.resolveAll(); builder.build(); return builder; }; /** * Loads a .json file and returns the Builder. * @param {string|{root: string, file: string}} filename Path to json file or an object specifying 'file' with * an overridden 'root' path for all imported files. * @param {function(ProtoBuf.Builder)=} callback Callback that will receive the Builder as its first argument. * If the request has failed, builder will be NULL. If omitted, the file will be read synchronously and this * function will return the Builder or NULL if the request has failed. * @param {ProtoBuf.Builder=} builder Builder to append to. Will create a new one if omitted. * @return {?ProtoBuf.Builder|undefined} The Builder if synchronous (no callback specified, will be NULL if the * request has failed), else undefined * @expose */ ProtoBuf.loadJsonFile = function(filename, callback, builder) { if (callback && typeof callback === 'object') { builder = callback; callback = null; } else if (!callback || typeof callback !== 'function') { callback = null; } if (callback) { ProtoBuf.Util.fetch(typeof filename === 'object' ? filename["root"]+"/"+filename["file"] : filename, function(contents) { try { callback(ProtoBuf.loadJson(JSON.parse(contents), builder, filename)); } catch (err) { callback(err); } }); } else { var contents = ProtoBuf.Util.fetch(typeof filename === 'object' ? filename["root"]+"/"+filename["file"] : filename); return contents !== null ? ProtoBuf.loadJson(JSON.parse(contents), builder, filename) : null; } }; return ProtoBuf; } // Enable module loading if available if (typeof module != 'undefined' && module["exports"]) { // CommonJS module["exports"] = loadProtoBuf(require("bytebuffer")); } else if (typeof define != 'undefined' && define["amd"]) { // AMD define("ProtoBuf", ["ByteBuffer"], loadProtoBuf); } else { // Shim if (!global["dcodeIO"]) { global["dcodeIO"] = {}; } global["dcodeIO"]["ProtoBuf"] = loadProtoBuf(global["dcodeIO"]["ByteBuffer"]); } })(this);