|
|
/** * 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 encoded 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 start = i + 1; while (str.charAt(++i) !== '-' && i != str.length) {} var buf = str.substring(start, i); 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)) { var start = i + 1; while (++i) { var c = str.charAt(i); if (',' === c) break; if (i === str.length) break; } p.nsp = str.substring(start, i); } else { p.nsp = '/'; }
// look up id
var next = str.charAt(i + 1); if ('' !== next && Number(next) == next) { var start = i + 1; while (++i) { var c = str.charAt(i); if (null == c || Number(c) != c) { --i; break; } if (i === str.length) break; } p.id = Number(str.substring(start, i + 1)); }
// 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 }; }
|