| /** | |
| * Module dependencies. | |
| */ | |
| var debug = require('debug')('socket.io-parser'); | |
| var Emitter = require('component-emitter'); | |
| var binary = require('./binary'); | |
| var isArray = require('isarray'); | |
| var isBuf = require('./is-buffer'); | |
| /** | |
| * Protocol version. | |
| * | |
| * @api public | |
| */ | |
| exports.protocol = 4; | |
| /** | |
| * Packet types. | |
| * | |
| * @api public | |
| */ | |
| exports.types = [ | |
| 'CONNECT', | |
| 'DISCONNECT', | |
| 'EVENT', | |
| 'ACK', | |
| 'ERROR', | |
| 'BINARY_EVENT', | |
| 'BINARY_ACK' | |
| ]; | |
| /** | |
| * Packet type `connect`. | |
| * | |
| * @api public | |
| */ | |
| exports.CONNECT = 0; | |
| /** | |
| * Packet type `disconnect`. | |
| * | |
| * @api public | |
| */ | |
| exports.DISCONNECT = 1; | |
| /** | |
| * Packet type `event`. | |
| * | |
| * @api public | |
| */ | |
| exports.EVENT = 2; | |
| /** | |
| * Packet type `ack`. | |
| * | |
| * @api public | |
| */ | |
| exports.ACK = 3; | |
| /** | |
| * Packet type `error`. | |
| * | |
| * @api public | |
| */ | |
| exports.ERROR = 4; | |
| /** | |
| * Packet type 'binary event' | |
| * | |
| * @api public | |
| */ | |
| exports.BINARY_EVENT = 5; | |
| /** | |
| * Packet type `binary ack`. For acks with binary arguments. | |
| * | |
| * @api public | |
| */ | |
| exports.BINARY_ACK = 6; | |
| /** | |
| * Encoder constructor. | |
| * | |
| * @api public | |
| */ | |
| exports.Encoder = Encoder; | |
| /** | |
| * Decoder constructor. | |
| * | |
| * @api public | |
| */ | |
| exports.Decoder = Decoder; | |
| /** | |
| * A socket.io Encoder instance | |
| * | |
| * @api public | |
| */ | |
| function Encoder() {} | |
| var ERROR_PACKET = exports.ERROR + '"encode error"'; | |
| /** | |
| * Encode a packet as a single string if non-binary, or as a | |
| * buffer sequence, depending on packet type. | |
| * | |
| * @param {Object} obj - packet object | |
| * @param {Function} callback - function to handle encodings (likely engine.write) | |
| * @return Calls callback with Array of encodings | |
| * @api public | |
| */ | |
| Encoder.prototype.encode = function(obj, callback){ | |
| debug('encoding packet %j', obj); | |
| if (exports.BINARY_EVENT === obj.type || exports.BINARY_ACK === obj.type) { | |
| encodeAsBinary(obj, callback); | |
| } else { | |
| var encoding = encodeAsString(obj); | |
| callback([encoding]); | |
| } | |
| }; | |
| /** | |
| * Encode packet as string. | |
| * | |
| * @param {Object} packet | |
| * @return {String} encoded | |
| * @api private | |
| */ | |
| function encodeAsString(obj) { | |
| // first is type | |
| var str = '' + obj.type; | |
| // attachments if we have them | |
| if (exports.BINARY_EVENT === obj.type || exports.BINARY_ACK === obj.type) { | |
| str += obj.attachments + '-'; | |
| } | |
| // if we have a namespace other than `/` | |
| // we append it followed by a comma `,` | |
| if (obj.nsp && '/' !== obj.nsp) { | |
| str += obj.nsp + ','; | |
| } | |
| // immediately followed by the id | |
| if (null != obj.id) { | |
| str += obj.id; | |
| } | |
| // json data | |
| if (null != obj.data) { | |
| var payload = tryStringify(obj.data); | |
| if (payload !== false) { | |
| str += payload; | |
| } else { | |
| return ERROR_PACKET; | |
| } | |
| } | |
| debug('encoded %j as %s', obj, str); | |
| return str; | |
| } | |
| function tryStringify(str) { | |
| try { | |
| return JSON.stringify(str); | |
| } catch(e){ | |
| return false; | |
| } | |
| } | |
| /** | |
| * Encode packet as 'buffer sequence' by removing blobs, and | |
| * deconstructing packet into object with placeholders and | |
| * a list of buffers. | |
| * | |
| * @param {Object} packet | |
| * @return {Buffer} encoded | |
| * @api private | |
| */ | |
| function encodeAsBinary(obj, callback) { | |
| function writeEncoding(bloblessData) { | |
| var deconstruction = binary.deconstructPacket(bloblessData); | |
| var pack = encodeAsString(deconstruction.packet); | |
| var buffers = deconstruction.buffers; | |
| buffers.unshift(pack); // add packet info to beginning of data list | |
| callback(buffers); // write all the buffers | |
| } | |
| binary.removeBlobs(obj, writeEncoding); | |
| } | |
| /** | |
| * A socket.io Decoder instance | |
| * | |
| * @return {Object} decoder | |
| * @api public | |
| */ | |
| function Decoder() { | |
| this.reconstructor = null; | |
| } | |
| /** | |
| * Mix in `Emitter` with Decoder. | |
| */ | |
| Emitter(Decoder.prototype); | |
| /** | |
| * Decodes an ecoded packet string into packet JSON. | |
| * | |
| * @param {String} obj - encoded packet | |
| * @return {Object} packet | |
| * @api public | |
| */ | |
| Decoder.prototype.add = function(obj) { | |
| var packet; | |
| if (typeof obj === 'string') { | |
| packet = decodeString(obj); | |
| if (exports.BINARY_EVENT === packet.type || exports.BINARY_ACK === packet.type) { // binary packet's json | |
| this.reconstructor = new BinaryReconstructor(packet); | |
| // no attachments, labeled binary but no binary data to follow | |
| if (this.reconstructor.reconPack.attachments === 0) { | |
| this.emit('decoded', packet); | |
| } | |
| } else { // non-binary full packet | |
| this.emit('decoded', packet); | |
| } | |
| } | |
| else if (isBuf(obj) || obj.base64) { // raw binary data | |
| if (!this.reconstructor) { | |
| throw new Error('got binary data when not reconstructing a packet'); | |
| } else { | |
| packet = this.reconstructor.takeBinaryData(obj); | |
| if (packet) { // received final buffer | |
| this.reconstructor = null; | |
| this.emit('decoded', packet); | |
| } | |
| } | |
| } | |
| else { | |
| throw new Error('Unknown type: ' + obj); | |
| } | |
| }; | |
| /** | |
| * Decode a packet String (JSON data) | |
| * | |
| * @param {String} str | |
| * @return {Object} packet | |
| * @api private | |
| */ | |
| function decodeString(str) { | |
| var i = 0; | |
| // look up type | |
| var p = { | |
| type: Number(str.charAt(0)) | |
| }; | |
| if (null == exports.types[p.type]) { | |
| return error('unknown packet type ' + p.type); | |
| } | |
| // look up attachments if type binary | |
| if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) { | |
| var buf = ''; | |
| while (str.charAt(++i) !== '-') { | |
| buf += str.charAt(i); | |
| if (i == str.length) break; | |
| } | |
| if (buf != Number(buf) || str.charAt(i) !== '-') { | |
| throw new Error('Illegal attachments'); | |
| } | |
| p.attachments = Number(buf); | |
| } | |
| // look up namespace (if any) | |
| if ('/' === str.charAt(i + 1)) { | |
| p.nsp = ''; | |
| while (++i) { | |
| var c = str.charAt(i); | |
| if (',' === c) break; | |
| p.nsp += c; | |
| if (i === str.length) break; | |
| } | |
| } else { | |
| p.nsp = '/'; | |
| } | |
| // look up id | |
| var next = str.charAt(i + 1); | |
| if ('' !== next && Number(next) == next) { | |
| p.id = ''; | |
| while (++i) { | |
| var c = str.charAt(i); | |
| if (null == c || Number(c) != c) { | |
| --i; | |
| break; | |
| } | |
| p.id += str.charAt(i); | |
| if (i === str.length) break; | |
| } | |
| p.id = Number(p.id); | |
| } | |
| // look up json data | |
| if (str.charAt(++i)) { | |
| var payload = tryParse(str.substr(i)); | |
| var isPayloadValid = payload !== false && (p.type === exports.ERROR || isArray(payload)); | |
| if (isPayloadValid) { | |
| p.data = payload; | |
| } else { | |
| return error('invalid payload'); | |
| } | |
| } | |
| debug('decoded %s as %j', str, p); | |
| return p; | |
| } | |
| function tryParse(str) { | |
| try { | |
| return JSON.parse(str); | |
| } catch(e){ | |
| return false; | |
| } | |
| } | |
| /** | |
| * Deallocates a parser's resources | |
| * | |
| * @api public | |
| */ | |
| Decoder.prototype.destroy = function() { | |
| if (this.reconstructor) { | |
| this.reconstructor.finishedReconstruction(); | |
| } | |
| }; | |
| /** | |
| * A manager of a binary event's 'buffer sequence'. Should | |
| * be constructed whenever a packet of type BINARY_EVENT is | |
| * decoded. | |
| * | |
| * @param {Object} packet | |
| * @return {BinaryReconstructor} initialized reconstructor | |
| * @api private | |
| */ | |
| function BinaryReconstructor(packet) { | |
| this.reconPack = packet; | |
| this.buffers = []; | |
| } | |
| /** | |
| * Method to be called when binary data received from connection | |
| * after a BINARY_EVENT packet. | |
| * | |
| * @param {Buffer | ArrayBuffer} binData - the raw binary data received | |
| * @return {null | Object} returns null if more binary data is expected or | |
| * a reconstructed packet object if all buffers have been received. | |
| * @api private | |
| */ | |
| BinaryReconstructor.prototype.takeBinaryData = function(binData) { | |
| this.buffers.push(binData); | |
| if (this.buffers.length === this.reconPack.attachments) { // done with buffer list | |
| var packet = binary.reconstructPacket(this.reconPack, this.buffers); | |
| this.finishedReconstruction(); | |
| return packet; | |
| } | |
| return null; | |
| }; | |
| /** | |
| * Cleans up binary packet reconstruction variables. | |
| * | |
| * @api private | |
| */ | |
| BinaryReconstructor.prototype.finishedReconstruction = function() { | |
| this.reconPack = null; | |
| this.buffers = []; | |
| }; | |
| function error(msg) { | |
| return { | |
| type: exports.ERROR, | |
| data: 'parser error: ' + msg | |
| }; | |
| } |